Pull to refresh

Linux контейнер для .NET Framework приложения (когда сложно уйти на .Net Core)

Reading time 7 min
Views 11K
Здравствуй, Хабр.

Хочу поделиться с миром достаточно нетипичной, по крайней мере для меня, задачкой и её решением, которое мне кажется вполне приемлемым. Описанное ниже, возможно, не является идеальным выходом из ситуации, но это работает, и работает так, как задумывалось.

Завязка и предпосылки


Появилась по работе задача: нужно на сайт вынести 3D-превьюшки BIM-моделей разного оборудования, материалов, объектов. Нужно что-то легковесное, несложное.

На сайте модели этих объектов хранятся и доступны для скачивания в проприетарных форматах различных САПР и в виде открытых форматов 3D-моделей. Среди них есть и формат IFC. Его-то я и буду использовать, как исходник для решения этого задания.

Один из вариантов исполнения и его особенности


Формально можно было бы ограничиться написанием какого-нибудь конвертера *.ifc во что-то для отображения на web-странице. С этого я и начал.

Для подобного преобразования был избран замечательный тулкит — xBIM Toolkit.

В примерах использования этого инструмента просто и доходчиво описано, как работать с IFC и специализированным для web-форматом *.wexBIM.

Сначала конвертируем *.ifc в *.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();
                }
            }
        }
    }
}


Далее полученный файл используется в «плеере» xBIM WeXplorer.

Пример встраивания *.wexBIM в страницу:
<!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>


Что ж, поехали. Беру nuget'ы от xBIM. Пишу консольное приложение, которое на вход принимает пачку путей к *.ifc-файлам, рядом с ними складывает пачку *.wexBIM-файлов. Всё, можно выкладывать на сайт.

Но как-то это простенько… Хочется, чтобы эта программа стала неким сервисом, который по событию загрузки *.ifc на портал, сразу создаёт необходимый *.wexBIM, и он сразу отображается в подготовленном контейнере.

Ок, формирую новые требования:

  1. пусть задания на конвертацию приходят от нашего RabbitMQ;
  2. сами задания хочу видеть в виде бинарного сообщения, которое на самом деле будет готовым для десериализации классом, описанным в protobuf-файле;
  3. задание будет содержать ссылку для скачивания исходного *.ifc-файла с нашего Minio;
  4. задание также будет сообщать мне, в какой bucket в Minio складывать результат;
  5. пусть само приложение будет собрано под .net core 3.1 и работет внутри Linux docker-контейнера на нашей «docker-ферме»;

Первые сложности и условности


Описывать подробно первые 4 пункта реализации не стану. Возможно позже.

Заставил приложение слушать очередь заданий и отсылать сообщение с результатом в очередь из CorrelationId сообщения-задания. Прикрутил генерированные классы запрос/ответ из protobuf. Научил скачивать/загружать файлы в minio.

Всё это делаю в проекте консольного приложения. В настройках проекта:

<TargetFramework>netcoreapp3.1</TargetFramework>

И на моей машине с Windows 10 всё вполне отлаживается и работает. Но при попытке запустить приложение в WSL ловлю ошибку System.IO.FileLoadException:

Полная информация по ошибке:
{
  "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
}

Сеанс активного гугления и вдумчивого чтения показал мне, что я крайне невнимателен:
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.

И подобные трудности не у одного меня. Многим хочется xBIM под .Net Core.
Не критично, но многое меняет… Всё упирается в невозможность нормально загрузить Xbim.Geometry.Engine64.dll. Нужно иметь на машине vc_redist.x64.exe. Какие у меня варианты?
Первое, что подумалось: «А может виндовый контейнер с полным .Net Framework использовать?
Доставить Microsoft Visual C++ Redistributable for Visual Studio 2015, 2017 and 2019 в этот контейнер, и всё будет ок?» Я это попробовал:

Испытательный Windows-образ для 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"]

Что ж, это сработало… It's alive! Но. А как же наша хостовая Linux-машина с docker? Не получится на неё загнать контейнер с образом на Windows Server Core. Надо выкручиваться…

Компромисс и развязка


Очередной поиск в Сети вывел меня на статью. В ней автор требует от реализации похожего:
To make things worse:
All binaries are 32-bits (x86).
Some require visual C++ redistributable runtime components.
Some require the .NET runtime.
Some need a windowing system, even though we only use the command-line interface (CLI).
В посте описывается потенциальная возможность запуска Windows-приложений в wine в Linux-контейнере. Любопытно, решил я.

После некоторых проб, багов и дополнений был получен Dockerfile:

Docker образ на основе Ubuntu с Wine, .Net Framework и vcredist на борту:

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

Build идёт небыстро, но заканчивается удачно. Пробую, проверяю. Работает!

Итоги, выводы, размышления


Это сработало. На выходе получаем Linux-образ для docker-контейнера. Он «пухловат» (~5.2Гб), но вполне быстро стартует и внутри работает консольное Windows-приложени на .Net Framework 4.7, которое слушает RabbitMQ, пишет логи в Graylog, скачивает и загружает файлы на/в Minio. Обновлять само приложение буду по remote docker API.

Решение утилитарной выделенной задачи реализовано. Возможно, и скорее всего, не универсальное. Но меня в принципе устроило. Может быть кому-то тоже пригодится.

Спасибо, что прочли. На Хабр пишу впервые. Увидимся в комментариях.
Tags:
Hubs:
+18
Comments 20
Comments Comments 20

Articles