1

My goal is to have a timer azure function (leveraging Selenium Chromedriver) run inside a docker container. it works in Visual Studio when i click this VS Docker Button; however, when i execute the same docker command from VS in the command line, nothing happens. here's the command VS executes

docker run -dt -v "C:\Users\rlin\vsdbg\vs2017u5:/remote_debugger:rw" -e "ASPNETCORE_ENVIRONMENT=Development" -p 34480:80 --name Retriever --entrypoint tail retriever -f /dev/null

here's my dockerfile for your reference

#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.

FROM mcr.microsoft.com/azure-functions/dotnet:3.0 AS base
WORKDIR /home/site/wwwroot
EXPOSE 80

FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
WORKDIR /home/site/wwwroot
COPY . Retriever/
WORKDIR /home/site/wwwroot/Retriever
RUN dotnet build -c Release -o /home/site/wwwroot

FROM build AS publish
RUN dotnet publish -c Release -o /home/site/wwwroot

FROM base AS final
WORKDIR /home/site/wwwroot
COPY --from=publish /home/site/wwwroot .
ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
    AzureFunctionsJobHost__Logging__Console__IsEnabled=true \
    AzureWebJobsStorage="StorageConnectionString"
    URL="url" \
    USERNAME="username" \
    PASSWORD="password" \
    SFTP="sftp" \
    CONTAINER="container"

WORKDIR /home/site/wwwroot
RUN apt-get update && \
    apt-get install -y gnupg wget curl unzip --no-install-recommends && \
    wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && \
    echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list && \
    apt-get update -y && \
    apt-get install -y google-chrome-stable && \
    CHROMEVER=$(google-chrome --product-version | grep -o "[^\.]*\.[^\.]*\.[^\.]*") && \
    DRIVERVER=$(curl -s "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_$CHROMEVER") && \
    wget -q --continue -P /chromedriver "http://chromedriver.storage.googleapis.com/$DRIVERVER/chromedriver_linux64.zip" && \
    unzip /chromedriver/chromedriver* -d /chromedriver

WORKDIR /
CMD ["/home/site/wwwroot/bin/Retriever.dll"]

been stuck on this for days, so any help is much appreciated.

EDIT: here's the code for ref

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using OpenQA.Selenium.Chrome;
using Microsoft.Extensions.Configuration;
using OpenQA.Selenium;
using System.Collections.Generic;
using Azure.Storage.Blobs;
using System.IO.Compression;

namespace Retriever
{
    public class Retriever
    {
        private IConfigurationRoot config;

        [FunctionName("Retriever")]
        public void Run(
            [TimerTrigger("0 */1 * * * *", RunOnStartup = true)] TimerInfo myTimer,
            //[HttpTrigger(AuthorizationLevel.System, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            config = new ConfigurationBuilder()
                .SetBasePath(Environment.CurrentDirectory)
                .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
                .AddEnvironmentVariables()
                .Build();

            Retrieve(log);
            log.LogInformation("Uploaded attachments");
            //return new OkResult();
        }
        public void Retrieve(ILogger log)
        {
            log.LogInformation("Retrieving attachments");

            ChromeOptions options = new ChromeOptions();
            options.AddArgument("--headless");
            options.AddArgument("--no-sandbox"); // Bypass OS security model
            options.AddArgument("start-maximized"); // open Browser in maximized mode
            options.AddArgument("disable-infobars"); // disabling infobars
            options.AddArgument("--disable-extensions"); // disabling extensions
            options.AddArgument("--disable-gpu"); // applicable to windows os only
            options.AddArgument("--disable-dev-shm-usage"); // overcome limited resource problems
            options.AddArgument("--whitelisted-ips");
            options.AddArgument("--proxy-server=direct://");
            options.AddArgument("--proxy-bypass-list=*");
            options.BinaryLocation = "/opt/google/chrome/google-chrome"; //Google Chrome path
            options.AddUserProfilePreference("download.prompt_for_download", false); // DO NOT prompt for download
            options.AddUserProfilePreference("download.default_directory", Path.GetTempPath()); // saves to Temp directory

            ChromeDriverService service = ChromeDriverService.CreateDefaultService("/chromedriver/", "chromedriver"); // use "/usr/bin/" and "chromedriver" in deployment

            ChromeDriver driver = new ChromeDriver(service, options, TimeSpan.FromMinutes(3));
            driver.Manage().Timeouts().ImplicitWait = new TimeSpan(5, 0, 0);

            // Login and Download Steps //
            driver.Navigate().GoToUrl(config.GetValue<string>("URL"));

            IWebElement username = driver.FindElement(By.XPath("//*[@id=\"username\"]"));
            username.SendKeys(config.GetValue<string>("USERNAME"));

            IWebElement next = driver.FindElement(By.CssSelector("body > mc-login > div > div.container > div > div > div.panel.panel-default.panel-shadow.login-panel > div > div > div > form > button"));
            next.Click();

            IWebElement password = driver.FindElement(By.CssSelector("#password"));
            password.SendKeys(config.GetValue<string>("PASSWORD"));

            IWebElement login = driver.FindElement(By.CssSelector("body > mc-login > div > div.container > div > div > div.panel.panel-default.panel-shadow.login-panel > div > div > div > form > button"));
            login.Click();

            IWebElement inbox = driver.FindElement(By.LinkText("Inbox"));
            inbox.Click();
            
            ICollection<IWebElement> emails = driver.FindElements(By.CssSelector("tr"));
            System.Threading.Thread.Sleep(new TimeSpan(0, 0, 2));
            foreach(IWebElement email in emails)
            {
                if (email.Displayed)
                {
                    email.Click();

                    IWebElement view = driver.FindElement(By.LinkText("View"));
                    view.Click();

                    string filename = driver.FindElement(By.CssSelector("body > div.page-container.with-sidebar.full-height.snap-content > div.main-content.full-height > div.ng-isolate-scope > div > div > div.container-fluid.full-height.fill-container.mainarea.ng-scope.mc-tab-unique-main > mc-list-detail > div > div.col-xs-12.col-sm-7.col-md-8.col-lg-8.hidden-xs.full-height.animate-active.no-padding > div > div.dynamic-full-height.horizontal-scroll.vertical-scroll.panel-half-margin-top-negative.ng-scope > div.pull-left.full-width.ng-scope > detail > div:nth-child(4) > div > mc-thumbnail > div > div:nth-child(2) > ul > li > span > span:nth-child(2)")).GetAttribute("innerHTML");
                    string filepath = Path.Combine(Path.GetTempPath(), filename);

                    IWebElement download = driver.FindElement(By.LinkText("Download"));
                    download.Click();
                    System.Threading.Thread.Sleep(new TimeSpan(0, 0, 2));
                    UploadToBlobStorage(log, filepath, filename);
                }
            }
            driver.Close();
            driver.Quit();
            driver.Dispose();
            service.Dispose();
        }
        public void UploadToBlobStorage(ILogger log, string filepath, string filename)
        {
            BlobServiceClient serviceClient = new BlobServiceClient(config.GetValue<string>("SFTP"));
            BlobContainerClient containerClient = serviceClient.GetBlobContainerClient(config.GetValue<string>("CONTAINER"));
            BlobClient client = containerClient.GetBlobClient("uploads/" + filename + ".txt");

            ZipArchive z = ZipFile.Open(filepath, ZipArchiveMode.Update);
            foreach(ZipArchiveEntry e in z.Entries)
            {
                client.Upload(e.Open(), overwrite: true);
            }
            z.Dispose();
            if(File.Exists(filepath)) { File.Delete(filepath); }
        }
    }
}

rlin
  • 13
  • 1
  • 4
  • would it help if i share my app's code? – rlin Dec 09 '20 at 15:32
  • if i only do this `docker run --name Retriever retriever:latest` i get `starting container process caused "exec: \"dotnet\": executable file not found in $PATH": unknown.`, which perplexes me, cuz azure-functions/dotnet:3.0 is supposed to have dotnet runtime – rlin Dec 09 '20 at 21:08
  • `mcr.microsoft.com/azure-functions/dotnet` does not contain a `dotnet` executable (you can check it yourself by opening a shell in a container image `docker run --rm -it mcr.microsoft.com/azure-functions/dotnet bash` and looking for it). But none should be needed. Does your app try to spawn a new `dotnet` process by any chance? – M. Dennhardt Dec 10 '20 at 10:41
  • @M. Dennhardt i've added app's code for clarity. please excuse my lack of understanding, but how do i tell if it tries to "spawn a new `dotnet` process"? – rlin Dec 10 '20 at 14:56
  • I was wondering if you try something like `Process.Start("dotnet")` in your app, which would fail because `dotnet` does not exist in the container. But this is not the case. I compared your Dockerfile with a the autogenerated one from a fresh project and am now wondering: Did you add `CMD ["/home/site/wwwroot/bin/Retriever.dll"]` (the last line) yourself? Because this is overwriting the default (correct?) CMD of the container and might be the root cause of your problem. Try removing it if it was added by you and not VS. – M. Dennhardt Dec 11 '20 at 08:38
  • Also `dotnet build` and `dotnet publish` both output to `/home/site/wwwroot` which is generally not a good idea. You want your publish output to not be mixed with the build output. – M. Dennhardt Dec 11 '20 at 08:42

1 Answers1

1

The command you posted is only used for Visual Studios debugging functionality. It overwites the entrypoint with tail -f /dev/null which is basically "loop forever".

You will need to run docker build <path to dir where Dockerfile is located> -t retriever to build the image and then docker run -p 80:80 retriever to run it. This should make the app available on port 80 of your system. If you want the app on another port, you can use -p <another port>:80

The last line (CMD ["/home/site/wwwroot/bin/Retriever.dll"]) in your Dockerfile overwrites the preset CMD of azure-functions/dotnet (/azure-functions-host/Microsoft.Azure.WebJobs.Script.WebHost) and will lead to the error "exec: \"dotnet\": executable file not found in $PATH". Your function is supposed to be run by WebHost, not by invoking the dll directly. Remove this line. The one above it is also unnecessary.

Your Dockerfile also has another issue (it will possibly still work without adressing them): You are copying your sources, building and publishing all to the same directory /home/site/wwwroot. I would suggest changing it like this for the build stage:

FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
WORKDIR /src
COPY . .
RUN dotnet build -c Release -o /app/build

For the publish stage:

FROM build AS publish
RUN dotnet publish -c Release -o /app/publish

And for final:

FROM base AS final
WORKDIR /home/site/wwwroot
COPY --from=publish /app/publish .
[...]
M. Dennhardt
  • 136
  • 7
  • hey @M. Dennhardt. thanks so much for the reply. this is my first SO post, so i'm super glad to get such a fast turnaround. – rlin Dec 09 '20 at 14:48
  • i tried `docker build "dockerfile directory" -t retriever:latest` then `docker run -P --name Retriever retriever:latest`, but the container exited immediately upon starting `CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 15dc302fecde retriever:latest "/home/site/wwwroot/…" 9 seconds ago Exited (1) 8 seconds ago Retriever` – rlin Dec 09 '20 at 14:52
  • OMG!!!!!!!!!! you're totally right. i had to comment out the last two lines cuz they were overwriting the WebHost. now everything's rainbows and butterflies again. thank you so much my man! – rlin Dec 11 '20 at 15:22