Contêiner Linux para o aplicativo .NET Framework (quando é difícil sair para o .Net Core)

Olá, Habr.

Quero compartilhar com o mundo uma tarefa bastante atípica, pelo menos para mim, e sua solução, que me parece bastante aceitável. Descrito abaixo pode não ser o caminho ideal para sair da situação, mas funciona e funciona conforme o esperado.

Configuração e plano de fundo


Havia uma tarefa a ser trabalhada: você precisa fazer visualizações em 3D de modelos BIM de vários equipamentos, materiais e objetos no site. Precisa de algo leve, sem complicações.

No site, os modelos desses objetos são armazenados e disponíveis para download em formatos proprietários de vários sistemas CAD e na forma de formatos abertos para modelos 3D. Entre eles está o formato IFC . Vou usá-lo como fonte para resolver esta tarefa.

Uma das opções e seus recursos


Formalmente, pode-se restringir a escrever algum tipo de conversor * .ifc para exibir algo em uma página da web. Aqui é onde eu comecei.

Um maravilhoso kit de ferramentas, o xBIM Toolkit, foi escolhido para essa conversão .

Os exemplos de uso dessa ferramenta descrevem de maneira simples e clara como trabalhar com o IFC e o * .wexBIM especializado no formato da Web.

Primeiro, converta * .ifc para * .wexBIM:
using System.IO;
using Xbim.Ifc;
using Xbim.ModelGeometry.Scene;

namespace CreateWexBIM
{
    class Program
    {
        public static void Main()
        {
            const string fileName = "SampleHouse.ifc";
            using (var model = IfcStore.Open(fileName))
            {
                var context = new Xbim3DModelContext(model);
                context.CreateContext();

                var wexBimFilename = Path.ChangeExtension(fileName, "wexBIM");
                using (var wexBiMfile = File.Create(wexBimFilename))
                {
                    using (var wexBimBinaryWriter = new BinaryWriter(wexBiMfile))
                    {
                        model.SaveAsWexBim(wexBimBinaryWriter);
                        wexBimBinaryWriter.Close();
                    }
                    wexBiMfile.Close();
                }
            }
        }
    }
}


Em seguida, o arquivo resultante é usado no xBIM WeXplorer "player" .

Um exemplo de incorporação * .wexBIM em uma página:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Hello building!</title>
    <script src="js/xbim-viewer.debug.bundle.js"></script>
</head>
<body>
    <div id="content">
        <canvas id="viewer" width="500" height="300"></canvas>
        <script type="text/javascript">
            var viewer = new xViewer('viewer');
            viewer.load('data/SampleHouse.wexbim');
            viewer.start();
        </script>
    </div>    
</body>
</html>


Bem, vamos. Eu tomo nugets de xBIM. Estou escrevendo um aplicativo de console que recebe vários caminhos para arquivos * .ifc como entrada e, ao lado deles, adiciona um monte de arquivos * .wexBIM. Tudo pode ser carregado no site.

Mas, de alguma forma, é bastante simples ... Quero que esse programa se torne um tipo de serviço que, no evento de upload * .ifc no portal, cria imediatamente o * .wexBIM necessário e aparece imediatamente no contêiner preparado.

Ok, eu formei novos requisitos:

  1. Deixe as tarefas de conversão virem do RabbitMQ ;
  2. Quero ver as próprias tarefas na forma de uma mensagem binária, que de fato estará pronta para desserialização pela classe descrita no arquivo protobuf ;
  3. a tarefa conterá um link para baixar o arquivo de origem * .ifc do nosso Minio ;
  4. a tarefa também me dirá qual balde no Minio para adicionar o resultado;
  5. deixe o próprio aplicativo ser construído sob o .net core 3.1 e trabalhar dentro do contêiner do docker Linux em nosso "farm de docker";

As primeiras dificuldades e convenções


Não descreverei detalhadamente os quatro primeiros pontos de implementação. Talvez mais tarde.

Faz com que o aplicativo escute a fila de trabalhos e envie uma mensagem com o resultado para a fila do CorrelationId da mensagem do trabalho. Foi danificada a classe de solicitação / resposta gerada pelo protobuf. Ministrado o download / upload de arquivos no minio .

Tudo isso eu faço no projeto de aplicativo do console. Nas configurações do projeto:

<TargetFramework>netcoreapp3.1</TargetFramework>

E na minha máquina com o Windows 10 tudo é completamente depurado e funciona. Mas quando tento executar o aplicativo no WSL, pego um erro System.IO.FileLoadException :

Informações de erro completas:
{
  "Type": "System.IO.FileLoadException",
  "Message": "Failed to load Xbim.Geometry.Engine64.dll",
  "TargetSite": "Void .ctor(Microsoft.Extensions.Logging.ILogger`1[Xbim.Geometry.Engine.Interop.XbimGeometryEngine])",
  "StackTrace": " at Xbim.Geometry.Engine.Interop.XbimGeometryEngine..ctor(ILogger`1 logger)\r\n at Xbim.Geometry.Engine.Interop.XbimGeometryEngine..ctor()\r\n at Xbim.ModelGeometry.Scene.Xbim3DModelContext.get_Engine()\r\n at Xbim.ModelGeometry.Scene.Xbim3DModelContext.CreateContext(ReportProgressDelegate progDelegate, Boolean adjustWcs)\r\n at My.Converter.ConvertIfc.CreateWebIfc(String ifcFileFullPath, String wexBIMFolder)",
  "Data": {},
  "InnerException": {
    "Type": "System.IO.FileNotFoundException",
    "Message": "Could not load file or assembly 'Xbim.Geometry.Engine.dll, Culture=neutral, PublicKeyToken=null'. The system cannot find the file specified.",
    "FileName": "Xbim.Geometry.Engine.dll, Culture=neutral, PublicKeyToken=null",
    "FusionLog": "",
    "TargetSite": "System.Reflection.RuntimeAssembly nLoad(System.Reflection.AssemblyName, System.String, System.Reflection.RuntimeAssembly, System.Threading.StackCrawlMark ByRef, Boolean, System.Runtime.Loader.AssemblyLoadContext)",
    "StackTrace": " at System.Reflection.RuntimeAssembly.nLoad(AssemblyName fileName, String codeBase, RuntimeAssembly assemblyContext, StackCrawlMark& stackMark, Boolean throwOnFileNotFound, AssemblyLoadContext assemblyLoadContext)\r\n at System.Reflection.RuntimeAssembly.InternalLoadAssemblyName(AssemblyName assemblyRef, StackCrawlMark& stackMark, AssemblyLoadContext assemblyLoadContext)\r\n at System.Reflection.Assembly.Load(String assemblyString)\r\n at Xbim.Geometry.Engine.Interop.XbimGeometryEngine..ctor(ILogger`1 logger)",
    "Data": {},
    "Source": "System.Private.CoreLib",
    "HResult": -2147024894
  },
  "Source": "Xbim.Geometry.Engine.Interop",
  "HResult": -2146232799
}

Uma sessão de pesquisa ativa e leitura cuidadosa me mostrou que eu era extremamente desatento:
Recently at work, we were evaluating a few options to render building models in the browser. Building Information Modeling (BIM) in interoperability scenarios is done via Industry Foundation Classes, mostly in the STEP Physical File format. The schema is quite huge and complex with all the things you have to consider, so we were glad to find the xBim open source project on GitHub. They've got both projects to visualize building models in the browser with WebGL as well as conversion tools to create the binary-formatted geometry mesh. To achieve that, native C++ libraries are dynamically loaded (so no .Net Core compatibility) which must be present in the bin folder. The C++ libraries are expected either in the same folder as the application binaries or in a x64 (or x86, respectively) sub folder (See here for more details). In regular projects, the xBim.Geometry NuGet package adds a build task to copy the dlls into the build output folder, but this doesn't work with the new tooling. You can, however, get it to work in Visual Studio 2015 by taking care of supplying the interop dlls yourself.

E dificuldades semelhantes não são apenas para mim. Muitas pessoas querem o xBIM no .Net Core .
Não é crítico, mas muda muito ... Tudo se resume à incapacidade de carregar o Xbim.Geometry.Engine64.dll normalmente . Você precisa ter vc_redist.x64.exe na máquina . Quais são as minhas opções?
A primeira coisa que pensei: “Um contêiner do Windows com um .NET Framework completo pode ser usado?
Entregue o Microsoft Visual C ++ Redistributable para Visual Studio 2015, 2017 e 2019 neste contêiner e tudo ficará bem? ” Eu tentei isso:

Teste a imagem do Windows para a janela de encaixe:
.Net Core :

<TargetFramework>net47</TargetFramework>

Dockerfile:

FROM microsoft/dotnet-framework:4.7
WORKDIR /bimlibconverter
COPY lib/VC_redist.x64.exe /VC_redist.x64.exe
RUN C:\VC_redist.x64.exe /quiet /install
COPY bin/Release .
ENTRYPOINT ["MyConverter.exe"]

Bem, funcionou ... Está vivo! Mas. Mas e a nossa máquina Linux host com docker? Não funcionará para direcionar um contêiner com uma imagem no Windows Server Core para ele . Preciso sair ...

Compromisso e desfecho


Outra pesquisa na Web me levou a um artigo . Nele, o autor requer uma implementação semelhante:
Para piorar as coisas:
todos os binários são de 32 bits (x86).
Alguns requerem componentes de tempo de execução redistribuíveis visuais do C ++.
Alguns requerem o tempo de execução do .NET.
Alguns precisam de um sistema de janelas, apesar de usarmos apenas a interface da linha de comandos (CLI).
A publicação descreve o potencial para a execução de aplicativos Windows no wine em um contêiner Linux. Curioso, eu decidi.

Após alguns testes, bugs e adições, o Dockerfile foi recebido:

Imagem de janela de encaixe baseada no Ubuntu com Wine, .Net Framework e vcredist a bordo:

FROM ubuntu:latest
#  x86
RUN dpkg --add-architecture i386 \
    && apt-get update \
    #   
    && apt-get install -qfy --install-recommends \
        software-properties-common \
        gnupg2 \
        wget \
        xvfb \
        cabextract \
    #  Wine
    && wget -nv https://dl.winehq.org/wine-builds/winehq.key \
    && apt-key add winehq.key \
    && apt-add-repository 'deb https://dl.winehq.org/wine-builds/ubuntu/ bionic main' \
    #     Wine
    && add-apt-repository ppa:cybermax-dexter/sdl2-backport \
    #  Wine
    && apt-get install -qfy --install-recommends \
        winehq-staging \
        winbind \
    # 
    && apt-get -y clean \
    && rm -rf \
      /var/lib/apt/lists/* \
      /usr/share/doc \
      /usr/share/doc-base \
      /usr/share/man \
      /usr/share/locale \
      /usr/share/zoneinfo
#    Wine
ENV WINEDEBUG=fixme-all
ENV WINEPREFIX=/root/.net
ENV WINEARCH=win64
#  Wine
RUN winecfg \
    # winetricks,   .Net Framework  
    && wget https://raw.githubusercontent.com/Winetricks/winetricks/master/src/winetricks \
    -O /usr/local/bin/winetricks \
    && chmod +x /usr/local/bin/winetricks \
# 
    && apt-get -y clean \
    && rm -rf \
      /var/lib/apt/lists/* \
      /usr/share/doc \
      /usr/share/doc-base \
      /usr/share/man \
      /usr/share/locale \
      /usr/share/zoneinfo \
    # Wine   
    && wineboot -u && winetricks -q dotnet472 && xvfb-run winetricks -q vcrun2015

WORKDIR /root/.net/drive_c/myconverter/

#  
COPY /bin/Release/ /root/.net/drive_c/myconverter/

ENTRYPOINT ["wine", "MyConverter.exe"]

UPD: . rueler

A construção não é rápida, mas termina com sucesso. Eu tento, verifique. Trabalho!

Resultados, conclusões, pensamentos


Funcionou. A saída é uma imagem do Linux para o contêiner do docker. É "inchado" (~ 5,2 GB), mas é iniciado rapidamente e por dentro executa um aplicativo de console do Windows no .Net Framework 4.7, que ouve RabbitMQ, grava logs no Graylog , baixa e carrega arquivos no / no Minio. Vou atualizar o aplicativo pela API do docker remoto.

A solução para a tarefa dedicada utilitária é implementada. Talvez, e muito provavelmente, não seja universal. Mas, em princípio, fiquei satisfeito. Talvez alguém venha a calhar também.

Obrigado pela leitura. Eu escrevo no Habr pela primeira vez. Vejo você nos comentários.

All Articles