experiments
July 9, 2023

За гранью возможного, опять

Очередная статья, призванная расширить границы вашего воображения без запрещенных препаратов. В этот раз мы соединим новое и очень старое: вызовем современный RESTful вебсервис из.. MS-DOS. И все это на Java под FreeBSD.

Да, это реальное рабочее окружение. Теперь понимаете почему мне не нужны препараты?

Саундтрек статьи

Постановка

Допустим есть некий софт, настолько старый что он был создан и успешно работал еще до вашего появления на свет.

Оборудование на котором хотя-бы в теории его можно запустить не выпускается лет так 20, даже общих разъемов, совместимых с современными компьютерами уже нет.

А софт нужный, нужно чтобы он продолжал работать.

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

Классический пример:

QuickBase в эмуляторе DOS, из-под Java.

Эмуляторы и эмуляция

При классической эмуляции на уровне оборудования, вы получите набор проблем, решения для которых в общем случае не существует:

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

В качестве примера: достаточно банальные переброс файлов и буфера обмена (Ctrl-C/Ctrl-V) в VirtualBox реализованы в виде аж отдельных драйверов, подгружаемых в гостевую систему. Естественно что гостевая система должна быть поддерживаемой, поскольку в нее загружается заранее написанный для нее же драйвер.

Типичный эмулятор железа — QEMU, VirtualBox, VMWare или Mame это в том или ином виде клиентская программа, где всегда есть механизмы передачи устройств ввода-вывода внутрь эмулируемой системы, с разграничением доступа.

Поэтому всех подобных эмуляторов есть интерфейс в виде окна, внутри которого отрисовывается картинка с виртуальной видеокарты, а нажатие клавиш на реальной клавиатуре просто транслируется внутрь эмулируемой системы.

Очевидно что это накладывает ограничения на возможности интеграции, в первую очередь в том что не существует простого способа вызвать какой-то метод на хосте и получить результат в гостевой ОС.

Обязательно придется лезть в эмуляцию какого-то устройства, которое будет прикидываться шлангом для гостевой ОС и выдавать нужный результат через три слоя эмулируемого API.

Но помимо эмуляции на уровне оборудования, на свете существует еще эмуляция окружения, хотя это и куда более редкий зверь.

Самым ярким примером такого подхода является проект Wine.

Ключевая особенность такого подхода к эмуляции, ради которого все и затевалось — широкий спектр возможностей по интеграции. При желании и навыках, вы можете интегрировать такой эмулятор в свой проект, организовав управление через API.

DOS и Dosbox

Оно же ДОС — Дисковая Операционная Система. Важная веха в истории ИТ, благодаря которой домашние и офисные компьютеры вообще получили такое распространение. Очевидно что за примерно 20 лет активного использования, под DOS было написана гора разнообразного софта, многое из которого используется до сих пор.

Еще на свете есть такая штука — Dosbox, интересна она тем что это самый настоящий эмулятор окружения DOS, обладающий так необходимыми нам возможностями встраивания и интеграции:

при запуске Dosbox подключает виртуальный диск Z:, все программы на котором на самом деле являются функциями самой платформы а не реальными бинарниками.

Да, это он — тот самый интерфейс прямого взаимодействия, до которого я очень давно хотел добраться.

Оригинальный Dosbox это нативное и достаточно большое приложение на Си, несмотря на весь мой интерес к теме — возиться с кастомной сборкой не очень хотелось, поэтому я пошел другим путем:

У проекта Dosbox есть форки — альтернативные реализации на других языках. Один из них — на Java, с куда меньшим объемом кода.

Именно он и был взят за основу тестового проекта интеграции.

Проект «Братишка»

Поскольку взятая за основу реализация Dosbox на Java оказалась слегка заброшенной, помимо того что исходный код оказался трансляцией переводом Гоблина с оригинала на Си — взял на себя смелость данный проект «слегка причесать».

Теперь он собирается из обычного Apache Maven а не х#й пойми как ручными средствами и работает на последних JDK.

По традиции все наработки выложены на Github.

Для сборки:

cd project
mvn clean install

Запуск:

java -jar target/box-1.0-SNAPSHOT.jar .

Передача точки в качестве аргумента нужна для того чтобы смонтировать текущую папку в виде диска С:

Вызов API через жопу вселенной

Теперь немного расскажу про то как вся эта йоба работает.

Виртуальные программы, запускаемые из внутреннего виртуального же диска Dosbox расположены в классе jdos.dos.Dos_programs. Класс большой (из-за трансляции с Си), поэтому привожу лишь выдержки.

За основу своей интеграции, я взял виртуальную программу «intro», которая просто отображает информацию о Dosbox и других командах.

Скелет внутреннего класса, реализующего логику «программы»:

private static class INTRO extends Program {
       ..
     public void Run() { 
       ..
      }
    }
    ..

Вот так выглядит регистрация вызова:

static private Program.PROGRAMS_Main INTRO_ProgramStart = new Program.PROGRAMS_Main() {
    public Program call() {
        return new INTRO();
    }
};

..и регистрация на виртуальном же диске:

Program.PROGRAMS_MakeFile("INTRO.COM",INTRO_ProgramStart);

В запущенном эмуляторе это выглядит вот так:

Результат вызова:

Клиент для вебсервиса

Ради максимального контраста между новым и старым, я использовал самый современный вариант вызова вебсервисов — с блекджеком и блудницами метаописанием на Yaml и кодогенерацией при сборке.

Для тестов был взят The Bored API — специальный вебсервис для ленивых оп#здолов, отдающий псевдослучайные данные на тему вариантов отдыха.

Единственный ленивый метод ленивого вебсервиса /api/activity, отдает вот такой ленивый JSON:

{"activity":"Teach your dog a new trick","type":"relaxation","participants":1,"price":0.05,"link":"","key":"1668223","accessibility":0.15}

Для этого ленивого метода был лениво написан Yaml-файл с метаданными:

swagger: "2.0"
info:
  title: "Boring API"
  version: "1.0.0"
  description: "Test API for testing"
  termsOfService: "None Available"
basePath: /api
schemes:
  - https
  - http
paths:
  /activity:
    get:
      summary: "Get current activity"
      description: "Returns current activity"
      produces:
        - application/json
      responses:
        200:
          description: "Exposure current activity"
          schema:
            $ref: '#/definitions/activity'
        404:
          description: "No exposure types found"          
definitions:
  activity:
    type: object
    properties:
      activity:
        type: string
        example: "some activity"
      type:
        type: string
        example: "relaxation"
      participants:
        type: number
        example: 1
      price:
        type: string
        format: float
        example: 0.25
      link:
        type: string
      key:
        type: string
      accessibility:
        type: string
        format: float

При сборке по этим метаданным с помощью плагина openapi-generator-maven-plugin генерируется клиентский код на Java, отвечающий за вызов этого вебсервиса.

Практически как с SOAP, только для хипстеров

Выдержка из pom.xml про настройку этого плагина:

<plugin>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator-maven-plugin</artifactId>
    <version>6.6.0</version>
    <executions>
        <execution>
            <goals>
                <goal>generate</goal>
            </goals>
            <configuration>
                <inputSpec>${project.basedir}/src/main/swagger/swagger.yaml</inputSpec>
                <generatorName>java</generatorName>
                <!-- использовать системный httpclient (java 11+) -->
                <library>native</library>
                <!-- пропуск генерации тестов API -->
                <generateApiTests>false</generateApiTests>
                <!-- пропуск генерации тестов моделей -->
                <generateModelTests>false</generateModelTests>
                <!— тонкая настройка генерации клиентского проекта -->
                <supportingFilesToGenerate>
                    RFC3339DateFormat.java,ApiCallback.java,ApiClient.java,ApiException.java,ApiResponse.java,Configuration.java,Pair.java,ProgressRequestBody.java,ProgressResponseBody.java,StringUtil.java,ApiKeyAuth.java,Authentication.java,HttpBasicAuth.java,JSON.java,OAuth.java,EncodingUtils.java
                </supportingFilesToGenerate>
                <!-- пропуск генерации документации API -->
                <generateApiDocumentation>false</generateApiDocumentation>
                <!-- пропуск генерации документации по моделям -->
                <generateModelDocumentation>false</generateModelDocumentation>
            </configuration>
        </execution>
    </executions>
</plugin>

После сборки в каталоге target/generated-sources/openapi будет сгенерирован REST-клиент для нашего ленивого сервиса, а исходники автоматически скомпилируются и попадут в итоговый JAR-файл.

Это самый модный и молодежный вариант использования вебсервисов

Вот так выглядит тестовый вызов этого вебсервиса (все классы ниже — генерированные):

public static void main(String[] args) throws ApiException {
    // создаем объект клиента
    ApiClient c = new ApiClient();
    // настраиваем подключение
    c.setScheme("https");
    c.setHost("www.boredapi.com");
    // создаем экземпляр сервиса API
    DefaultApi d = new DefaultApi(c);
    // делаем вызом
    Activity a=  d.activityGet();
    // вывод на экран
    System.out.println("a: "+a.getActivity());
   }   
Дерни деда за API

Old & young

Наконец переходим к тому самому — сочленению двух разных миров: сделаем вызов из ДОС современного вебсервиса.

Вот так выглядит класс, реализующий виртуальную программу для Dosbox:

private static class WS_CALL extends Program {
    public void Run() {
        try {
            ApiClient c = new ApiClient();
            c.setScheme("https");
            c.setHost("www.boredapi.com");
            DefaultApi d = new DefaultApi(c);
            Activity a = d.activityGet();
            System.out.printf("WS Response: %s%n", a.getActivity());
            WriteOut_NoParsing(String.format("response: %s", a.getActivity()));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Регистрация:

Program.PROGRAMS_MakeFile("REST.COM", WS_CALL::new);

Это все внутри того же общего класса jdos.dos.Dos_programs который я уже описывал выше, просто он действительно очень большой.

Вот так выглядит вызов нашего замечательного вебсервиса из эмулятора:

Дальшнейшие изыскания

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

Для реального проекта под ДОС, требующего интеграции с современной ИТ-инфраструктурой, я бы пошел по более традиционному пути в виде использования COM-портов или сетевого стека — все же это более реальный вариант чем описанная дичь.