Como implementar binários do Chromium através da rede

Em alguns casos, é necessário tornar o instalador ou o pacote de implementação tão pequeno quanto possível - por exemplo, pode haver uma restrição do serviço onde a aplicação é publicada.

Por outro lado, o DotNetBrowser funciona com seu próprio mecanismo Chromium, e os binários devem ser implantados no ambiente de destino. A abordagem mais direta é fornecer os binários como parte do instalador, no entanto, isto levará a um aumento do tamanho do próprio instalador.

Uma das soluções possíveis é fornecer os binários do Chromium pela rede assim que o DotNetBrowser tentar localizá-los. O DotNetBrowser utiliza a lógica de carregamento de assemblagem .NET predefinida para localizar e carregar a DLL dos binários, então é possível utilizar o evento AppDomain.AssemblyResolve para personalizar o processo global e fornecer a DLL de uma forma não normalizada.

Eis a descrição geral da ideia:

  1. Registrar um handler personalizado do evento AppDomain.AssemblyResolve.
  2. Neste handler, filtre as tentativas relacionadas com os binários DotNetBrowser.
  3. Quando o evento AssemblyResolve correspondente for recebido, utilize o nome da montagem totalmente qualificado para preparar um pedido de rede.
  4. Execute o pedido e obtenha a DLL como um array de bytes.
  5. Carregue o DLL a partir dos bytes e devolva-o a partir do handler.

O código da aplicação de exemplo que executa as ações descritas para carregar os binários DLL está disponível no repositório GitHub como projetos Visual Studio: C#, VB.NET

O exemplo em si é uma aplicação WPF, mas a abordagem não é específica do WPF e pode ser utilizada também em aplicações WinForms e de console.

A seção seguinte analisa a implementação e explica as partes mais importantes do exemplo.

Implementação

Classe BinariesResolverBase

A classe abstrata BinariesResolverBase fornece uma implementação geral do resolvedor de binários. O construtor da classe inicializa a propriedade RequestUri, que pode ser utilizada mais tarde para preparar pedidos, cria uma instância HttpClient para efetuar esses pedidos e inscreve o evento AssemblyResolve do domínio da aplicação.

protected BinariesResolverBase(string requestUri, AppDomain domain = null)
{
    if (domain == null)
    {
        domain = AppDomain.CurrentDomain;
    }

    RequestUri = requestUri;
    client = new HttpClient();
    domain.AssemblyResolve += Resolve;
}

O método Resolve(object sender, ResolveEventArgs args) trata os eventos AssemblyResolve e filtra os pedidos de montagens com nomes que começam por “DotNetBrowser.Chromium”. Para estas montagens, ele chama uma sobrecarga do método privado Resolve para o processamento posterior.

public Assembly Resolve(object sender, ResolveEventArgs args)
    => args.Name.StartsWith("DotNetBrowser.Chromium") ? Resolve(args.Name).Result : null;

A sobrecarga do método Resolve(string binariesAssemblyName) cria uma instância AssemblyName e transmite-a ao método abstrato PrepareRequest(AssemblyName assemblyName) para preparar a cadeia de pedidos URL. Depois disso, a cadeia de pedidos URL é utilizada para efetuar o pedido real através do HttpClient. Espera-se que o fluxo de resposta contenha os bytes do conjunto de binários do Chromium. Estes bytes são então passados para o método abstrato ProcessResponse(Stream responseBody, AssemblyName assemblyName) que devolve a montagem em si.

private async Task<Assembly> Resolve(string binariesAssemblyName)
{
    //Note: as montagens são normalmente resolvidas na thread de fundo da aplicação IU.
    try
    {
        //Construir um pedido utilizando o nome de montagem totalmente qualificado.
        AssemblyName assemblyName = new AssemblyName(binariesAssemblyName);
        string request = PrepareRequest(assemblyName);

        //Faz o pedido e baixa a resposta.
        OnStatusUpdated("Descarregamento dos binários do Chromium...");
        Debug.WriteLine($"Descarregamento {request}");
        HttpResponseMessage response = await client.GetAsync(request);

        response.EnsureSuccessStatusCode();
        OnStatusUpdated("Chromium binaries package downloaded");
        Stream responseBody = await response.Content.ReadAsStreamAsync();

        //Processa os bytes de resposta e carrega a montagem.
        return ProcessResponse(responseBody, assemblyName);
    }
    catch (Exception e)
    {
        Debug.WriteLine("Exception caught: {0} ", e);
    }

    return null;
}

O método OnStatusUpdated gera o evento StatusUpdated com uma mensagem específica, que pode então ser utilizada para ser informada sobre o progresso nas outras partes da aplicação.

A implementação abstrata BinariesResolverBase é depois alargada pela classe BinariesResolver que fornece as implementações dos métodos PrepareRequest e ProcessResponse .

Classe BinariesResolver

O BinariesResolver resolve a montagem de binários do Chromium baixando o arquivo de distribuição do DotNetBrowser e extraindo a montagem necessária a partir dele em tempo real. Esta classe é derivada de BinariesResolverBase e fornece as implementações dos seus membros abstratos.

Aqui está a implementação PrepareRequest:

protected override string PrepareRequest(AssemblyName assemblyName)
{
    //Utiliza apenas os componentes de versão maior e menor se o componente de compilação for 0.
    int fieldCount = assemblyName.Version.Build == 0 ? 2 : 3;
    return string.Format(RequestUri, assemblyName.Version.ToString(fieldCount));
}

Esta implementação reutiliza a propriedade RequestUri para preparar a referência direta ao arquivo de distribuição, dependendo da versão do DotNetBrowser. O RequestUri, neste caso, é definido como UriTemplate, que tem o seguinte aspecto:

private const string UriTemplate =
    "https://storage.googleapis.com/cloud.teamdev.com/downloads/"
    +"dotnetbrowser/{0}/dotnetbrowser-net45-{0}.zip";

Depois que o arquivo de distribuição ser recebido como um fluxo, o método ProcessResponse é chamado. A implementação deste método extrai o DLL em tempo real e carrega-o diretamente no domínio da aplicação atual. A montagem carregada é então utilizada como valor de retorno.

protected override Assembly ProcessResponse(Stream responseBody, AssemblyName assemblyName)
{
    // Os bytes baixados representam um arquivo ZIP. Localize a DLL que precisamos neste arquivo.
    ZipArchive archive = new ZipArchive(responseBody);
    ZipArchiveEntry binariesDllEntry 
        = archive.Entries
                .FirstOrDefault(entry => entry.FullName.EndsWith(".dll")
                                        && entry.FullName.Contains(assemblyName.Name));
    if (binariesDllEntry == null)
    {
        return null;
    }

    // Descompactar a entrada encontrada e carregar a DLL.
    OnStatusUpdated("Unzipping Chromium binaries");
    Stream unzippedEntryStream;
    using (unzippedEntryStream = binariesDllEntry.Open())
    {
        using (MemoryStream memoryStream = new MemoryStream())
        {
            unzippedEntryStream.CopyTo(memoryStream);
            OnStatusUpdated("Loading Chromium binaries assembly");
            Assembly assembly = Assembly.Load(memoryStream.ToArray());
            OnStatusUpdated("Chromium binaries assembly loaded.", true);
            return assembly;
        }
    }
}

Depois de carregar a DLL no domínio do aplicativo, o DotNetBrowser descompacta os binários dessa DLL para a pasta configurada por meio do EngineOptions.Builder.ChromiumDirectory e, em seguida, inicia o processo do Chromium como de costume.

O arquivo de distribuição do DotNetBrowser aqui é usado apenas como um exemplo. Se você planeja implementar uma abordagem semelhante na sua aplicação, deve considerar implementar o seu próprio serviço que fornece a DLL necessária para evitar a utilização excessiva de memória e diminuir o tempo de inicialização.

Vantagens

A principal vantagem da abordagem sugerida é minimizar o tamanho do pacote do aplicativo distribuído - não há necessidade de fornecer as DLLs DotNetBrowser.Chromium como parte do instalador ou do pacote de distribuição.

Desvantagens

  1. Aumento do tempo de inicialização. Quando os binários do Chromium são fornecidos através da rede, a inicialização incluirá o download da DLL dos binários, o que normalmente demora algum tempo, mesmo com uma boa conexão. Se a conexão for ruim, podem ser necessárias algumas tentativas e o tempo de inicialização aumentará significativamente.
  2. É necessária uma ligação à Internet. Se não houver conexão com a Internet, não será possível fazer o download dos binários e inicializar o mecanismo DotNetBrowser.
  3. A utilização da memória é aumentada. Se as DLLs DotNetBrowser.Chromium forem carregadas a partir do array de bytes, o DotNetBrowser descompacta os binários na memória e o uso da memória aumentará durante a inicialização. Isto pode até levar a problemas de falta de memória em ambientes de 32 bits se a utilização de memória já for suficientemente elevada.

Resumo

A abordagem descrita pode parecer útil para os tipos específicos de soluções, no entanto, é necessário considerar cuidadosamente os seus prós e contras antes de tomar uma decisão final.

Go Top