Linux-Container für die .NET Framework-Anwendung (wenn es schwierig ist, für .Net Core zu gehen)

Hallo Habr.

Ich möchte mit der Welt eine eher untypische Aufgabe teilen, zumindest für mich, und ihre Lösung, die mir durchaus akzeptabel erscheint. Das unten beschriebene ist möglicherweise nicht der ideale Ausweg aus der Situation, aber es funktioniert und funktioniert wie beabsichtigt.

Einstellung und Hintergrund


Es gab eine Aufgabe für die Arbeit: Sie müssen eine 3D-Vorschau von BIM-Modellen verschiedener Geräte, Materialien und Objekte auf der Site erstellen. Benötigen Sie etwas Leichtes, Unkompliziertes.

Auf der Website werden Modelle dieser Objekte gespeichert und stehen in proprietären Formaten verschiedener CAD-Systeme und in Form offener Formate für 3D-Modelle zum Download zur Verfügung . Darunter befindet sich das IFC- Format . Ich werde es als Quelle für die Lösung dieser Aufgabe verwenden.

Eine der Optionen und ihre Funktionen


Formal könnte man sich darauf beschränken, eine Art * .ifc-Konverter zu schreiben, damit etwas auf einer Webseite angezeigt wird. Hier habe ich angefangen.

Ein wunderbares Toolkit, das xBIM Toolkit, wurde für eine solche Konvertierung ausgewählt .

Die Beispiele für die Verwendung dieses Tools beschreiben einfach und klar , wie mit IFC und dem auf das Webformat spezialisierten * .wexBIM gearbeitet wird.

Konvertieren Sie zuerst * .ifc in * .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();
                }
            }
        }
    }
}


Als nächstes wird die resultierende Datei im "Player" xBIM WeXplorer verwendet .

Ein Beispiel für das Einbetten von * .wexBIM in eine Seite:
<!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>


Also, lasst uns gehen. Ich nehme Nugets von xBIM. Ich schreibe eine Konsolenanwendung, die eine Reihe von Pfaden zu * .ifc-Dateien als Eingabe empfängt und daneben eine Reihe von * .wexBIM-Dateien hinzufügt. Alles kann auf die Seite hochgeladen werden.

Aber irgendwie ist es ganz einfach ... Ich möchte, dass dieses Programm zu einer Art Dienst wird, der beim Hochladen des Ereignisses * .ifc in das Portal sofort das erforderliche * .wexBIM erstellt und sofort im vorbereiteten Container angezeigt wird.

Ok, ich bilde neue Anforderungen:

  1. Lassen Sie Konvertierungsaufgaben von unserem RabbitMQ kommen ;
  2. Ich möchte die Aufgaben selbst in Form einer binären Nachricht sehen, die tatsächlich von der in der Protobuf- Datei beschriebenen Klasse zur Deserialisierung bereit ist .
  3. Die Aufgabe enthält einen Link zum Herunterladen der Quelldatei * .ifc von unserem Minio .
  4. Die Aufgabe sagt mir auch, welcher Bucket in Minio das Ergebnis hinzufügen soll.
  5. Lassen Sie die Anwendung selbst unter .net Core 3.1 erstellen und arbeiten Sie im Linux-Docker-Container auf unserer "Docker-Farm".

Die ersten Schwierigkeiten und Konventionen


Ich werde die ersten 4 Umsetzungspunkte nicht im Detail beschreiben. Vielleicht später.

Die Anwendung hat die Jobwarteschlange abgehört und eine Nachricht mit dem Ergebnis aus der Korrelations- ID der Jobnachricht an die Warteschlange gesendet. Verschraubte die generierten Anforderungs- / Antwortklassen von protobuf. Unterrichtet, um Dateien in Minio herunterzuladen / hochzuladen .

All das mache ich im Konsolenanwendungsprojekt. In den Projekteinstellungen:

<TargetFramework>netcoreapp3.1</TargetFramework>

Und auf meinem Computer mit Windows 10 ist alles vollständig debuggt und funktioniert. Wenn ich jedoch versuche, die Anwendung in WSL auszuführen, wird ein System.IO.FileLoadException- Fehler angezeigt :

Vollständige Fehlerinformationen:
{
  "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
}

Eine Sitzung mit aktivem Googeln und nachdenklichem Lesen zeigte mir, dass ich äußerst unaufmerksam war:
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.

Und ähnliche Schwierigkeiten gibt es nicht nur für mich. Viele Leute wollen xBIM unter .Net Core .
Nicht kritisch, aber es ändert sich sehr ... Alles hängt von der Unfähigkeit ab, Xbim.Geometry.Engine64.dll normal zu laden . Sie müssen vc_redist.x64.exe auf dem Computer haben . Was sind meine Optionen?
Das erste, was ich dachte: „Kann ein Windows-Container mit einem vollständigen .Net Framework verwendet werden?
Stellen Sie Microsoft Visual C ++ Redistributable für Visual Studio 2015, 2017 und 2019 in diesen Container bereit, und alles wird in Ordnung sein? “ Ich habe es versucht:

Testen Sie das Windows-Image für Docker:
.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"]

Nun, es hat funktioniert ... Es lebt! Aber. Aber was ist mit unserer Host-Linux-Maschine mit Docker? Es funktioniert nicht, einen Container mit einem Image auf Windows Server Core darauf zu fahren . Muss raus ...

Kompromiss und Auflösung


Eine weitere Suche im Web brachte mich zu einem Artikel . Darin verlangt der Autor eine ähnliche Implementierung:
Um die Sache noch schlimmer zu machen:
Alle Binärdateien sind 32-Bit (x86).
Einige erfordern visuell umverteilbare C ++ - Laufzeitkomponenten.
Einige erfordern die .NET-Laufzeit.
Einige benötigen ein Fenstersystem, obwohl wir nur die Befehlszeilenschnittstelle (CLI) verwenden.
Der Beitrag beschreibt das Potenzial für die Ausführung von Windows-Anwendungen in Wine in einem Linux-Container. Neugierig entschied ich mich.

Nach einigen Tests, Fehlern und Ergänzungen wurde die Docker-Datei empfangen:

Ubuntu-basiertes Docker-Image mit Wine, .Net Framework und vcredist an Bord:

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

Der Build ist nicht schnell, endet aber erfolgreich. Ich versuche es. Funktioniert!

Ergebnisse, Schlussfolgerungen, Gedanken


Es funktionierte. Die Ausgabe ist ein Linux-Image für den Docker-Container. Es ist "aufgedunsen" (~ 5,2 GB), startet jedoch recht schnell und führt eine Windows-Konsolenanwendung auf dem .Net Framework 4.7 aus, die RabbitMQ abhört, Protokolle in Graylog schreibt, Dateien herunterlädt und in / in Minio hochlädt. Ich werde die Anwendung über die Remote-Docker-API aktualisieren.

Die Lösung für die utilitaristische Aufgabe wird umgesetzt. Vielleicht und höchstwahrscheinlich nicht universell. Aber im Prinzip war ich zufrieden. Vielleicht ist auch jemand nützlich.

Danke fürs Lesen. Ich schreibe zum ersten Mal über Habr. Wir sehen uns in den Kommentaren.

All Articles