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:
- Registrar um handler personalizado do evento AppDomain.AssemblyResolve.
- Neste handler, filtre as tentativas relacionadas com os binários DotNetBrowser.
- Quando o evento AssemblyResolve correspondente for recebido, utilize o nome da montagem totalmente qualificado para preparar um pedido de rede.
- Execute o pedido e obtenha a DLL como um array de bytes.
- 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
- 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.
- É 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.
- 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.