Работа с периферией из JavaScript: от теории к практике

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

    Статья будет полезна архитекторам и опытным front-end разработчикам систем масштаба предприятий, столкнувшихся с проблемой доступа к периферийному оборудованию из тонкого клиента своей системы.



    И да, скажем сразу, задача усложняется тем, что стандартные подходы с применением ActiveX, Java Applet, плагина браузера нас не устраивают по соображениям безопасности, универсальности и сложностей с  управляемостью и сопровождаемостью.

    Представьте себе скромный (по китайским меркам) банк, в отделениях которого работают более 100 тыс. операторов. У них есть рабочие места, на которых они обслуживают клиентов, а  к рабочим местам подключены различные периферийные устройства:

    1. Сетевой/локальный принтер.
    2. Чековый принтер (Epson-M950 или Olivetti).
    3. POS-терминал (Verifone VX820).
    4. Устройство для чтения «таблеток» (touch memory с ЭЦП).
    5. Сканеры разных типов (для штрих-кодов, паспортов или просто документов).
    6. Веб-камера (фотографировать потенциальных заемщиков).
    7. Специфическое банковское оборудование – cash dispenser/receiver и т.п.

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

    Работа с устройствами на низком уровне осуществляется через драйвера производителей оборудования или native библиотеки внутренней разработки. Установка и обновление драйверов и другого ПО на рабочих местах осуществляются централизованно. На рабочих местах стоят Windows и Internet Explorer 11 или 8. Обсуждается возможность перехода на Linux и Chrome/Firefox. Отсюда возникает требование кросс-платформенности и кросс-браузерности.

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

    Требования безопасности заключаются в контроле целостности кода, запускаемого на клиенте, ограничении доступа к периферии (доступ должен быть только с локального рабочего места) и ряда специфичных требований к работе с touch memory.

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

    Как мы пытались все это приручить, почему не сразу получилось


    Из условий эксплуатации следует схема работы с периферией:

    Мы попытались найти решение проблемы и проработали несколько вариантов.

    HTML5


    Хотя в нем потенциально есть работа с периферийными устройствами, текущие реализации заточены под мобильные устройства или аудио/видео,  и для наших задач не подходят от слова «совсем».

    WebUSB


    https://wicg.github.io/webusb/ — во-первых, не все устройства подключаются через USB. Во-вторых, поддержки WebUSB на текущий момент нет нигде, кроме экспериментальной feature в Chrome. Так что тоже нам не подходит.

    ActiveX


    Способ старинный и проверенный – делаются обертки над драйверами, устанавливаемые как локальные OCX или DLL, обращение к которым идет через ActiveXObject:

    var printer = new ActiveXObject("LocalPrinterComponent.Printer");
    printer.print(data + "\r\n");

    Но он работает только в IE и Windows. Несмотря на попытки сделать технологию ActiveX переносимой,  Microsoft отказалась от развития ActiveX в пользу технологии плагинов.

    Плагины браузера


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

    Нативные компоненты на Java


    На localhost выставляется cервис, который доступен из JavaScript. От этого тоже решили отказаться, т.к. реализация более трудоемкая, требуется использовать веб-сервер либо писать свой.

    Applet


    Не целевой, возникают проблемы с правами доступа на локальном устройстве.

    И что же делать?


    В итоге мы остановились на следующей архитектуре работы с периферийными устройствами:



    Клиентское приложение в JavaScript обращается к локальному сервису через модуль работы с периферийным API, который является обвязкой над клиентской частью socket.io.

    На рабочие места устанавливается node.js, который запускается под сервисной учетной записью при старте операционки. В node.js работает наш модуль bootstrap, отвечающий за загрузку npm-модулей для работы с периферийными устройствами с сервера в локальную файловую систему. Клиентский код генерирует event, в качестве атрибутов передается код и версия модуля, который работает с устройством, вызываемый метод и его параметры:

    <html>
    <head>
        <title>Test hello.socket.io</title>
        <script src="socket.io.min.js"></script>
        <script>
            var socket = io.connect('http://localhost:8055');
            socket.on('eSACExec', function (data) {
                var newLi = document.createElement('li');
                newLi.style.color = "green";
                newLi.innerHTML = data.data + ' from ' + data.from;
                document.getElementById("list").appendChild(newLi);
                console.log(data.data);
            });
            socket.on('eSACOnError', function (data) {
                var newLi = document.createElement('li');
                newLi.style.color = "red";
                newLi.innerHTML = data.message;
                document.getElementById("list").appendChild(newLi);
                console.log(data.error);
            });
            function sendHello() {
                socket.emit('eSASExec', {module: 'api.system.win.window', version:'0.0.1',
                    repoURL: 'git+https://github.com/Gromatchikov/',
                    method: 'sayHello', params: {hello: 'Server API'}});
            }
        </script>
    </head>
    <body>
        <ol id="list">
        </ol>
        <button onclick="sendHello()">send</button>
    </body>
    </html>

    Также bootstrap отвечает за работу с платформенными сервисами (выгрузку логов с рабочего места по запросу администратора и т.п.)

    Для каждого периферийного устройства  имеется свой модуль работы с ним, исполняемый в node.js. Bootstrap проксирует вызов в метод модуля:

    var http = require('http');
    var sio = require('socket.io');
    var bootstrapSA = require('./bootstrap.js');
    var modules = new Object();
     
    function SAServer() {
        if (!(this instanceof SAServer)) {
            return new SAServer();
        }
     
        this.app = http.createServer();
        this.sioApp = sio.listen(this.app);
     
        this.sioApp.sockets.on('connection', function (client) {
            client.on('eSASExec', function (data) {
                try {
                    console.info('SAserver event eSASExec:');
                    console.info(data);
     
                    bootstrapSA.demandModuleProperty(modules, data.repoURL, data.module, data.version, function (module) {
                        var fullModuleName = bootstrapSA.getFullName(data.module, data.version)
                        var api = modules[fullModuleName];
                        if (api != undefined) {
                            var result = api[data.method](data.params);
                            console.info('Result eSASExec', result);
                            client.emit('eSACExec', {data: result || '', from: fullModuleName});
                        } else {
                            console.error('[', 'Error ', fullModuleName, '] is ', undefined);
                            client.emit('eSACOnError', { message: 'Error define api module' + fullModuleName});
                        }
                    });
                } catch (_error) {
                    console.error('[', 'Error eSASExec', ( _error.code ? _error.code : ''), ']', _error);
                    client.emit('eSACOnError', { message: 'Error :(', error: _error.stack || ''});
                }
            });
            client.on('disconnect', () => {
                console.log('User disconnected');
            });
            client.on('eSASStop', () => {
                process.exit(0);
            });
            console.info('User connected');
        });
        console.info('System API server loaded');
    }
     
    //запуск сервера
    SAServer.prototype.start = function startSAServer(port) {
        // диалоговое окно
        this.currentPort = port;
        if (this.currentPort == undefined) {
            this.currentPort = process.env.npm_package_config_port;
        }
        this.app.listen(this.currentPort);
        console.log('System API server running at http://127.0.0.1:'+ this.currentPort);
     
        bootstrapSA.inspect('parent object', modules);
    };
     
    module.exports = SAServer;
    

    Модуль работает с низкоуровневым API операционной системы или драйвера через npm модуль node.js «ffi»:

    // функция преобразования строки JavaScript (UTF-8) в UTF-16
    function TEXT(text){
        return new Buffer(text, 'ucs2').toString('binary');
    }
     
    var FFI = require('ffi');
     
    // подключаемся к user32.dll
    var user32 = new FFI.Library('user32', {
        'MessageBoxW': [
            'int32', [ 'int32', 'string', 'string', 'int32' ]
        ]
    });
     
    function WindowSA() {
        if (!(this instanceof WindowSA)) {
            return new WindowSA();
        }
     
        console.log('Window system API module loaded');
    }
     
    WindowSA.prototype.sayHello = function sayHello(params) {
        // диалоговое окно
        var OK_or_Cancel = user32.MessageBoxW(0, TEXT('Привет, "' + params.hello + '"!'), TEXT('Заголовок окна'), 1);
    };
     
    module.exports = WindowSA;
    

    Когда клиентское приложение обращается к bootstrap, передавая модуль и версию, bootstrap проверяет локальное хранилище. Если нужного модуля нет в локальном хранилище, он выкачивается его с сервера. Таким образом, централизованно устанавливаются только драйвера и node.js с bootstrap’ом, а npm-модули для работы с устройствами скачиваются в рантайме. Но данная функция вряд ли будет использоваться в промышленной конфигурации, так что предполагаем, что при удаленной инсталляции драйверов устройств на рабочие места сотрудников будет устанавливаться и соответствующий npm модуль периферийного API, представляющий собой JS bundle.

    var loadModule = function (obj, repoURL, name, version, callback) {
        var mod;
        try {
            //todo mod = require(fullModuleName); и ограничить версию major.minor для накатки fix
            console.log('System API module "%s" require...', getFullName(name, version));
            mod = new require(name)();
            return mod;
        } catch (err){
            errFlag = true;
            console.error('Error require', err.code, err);
            if (err.code == 'MODULE_NOT_FOUND') {
                installModule(obj, repoURL, name, version, callback);
            }
        }
    }
     
    //Установить свойство и модуль, если ранее не был установлен
    //todo установка зависимости на модуль с версией name@version (fullModuleName)
    var demandModuleProperty = function (obj, repoURL, name, version, callback) {
        var fullModuleName = getFullName(name, version);
        var errFlag = false;
        if (!obj.hasOwnProperty(fullModuleName)) {
            console.log('System API module "%s" defining', fullModuleName);
            var mod = loadModule(obj, repoURL, name, version, callback);
            if (mod == undefined){
                return;
            }
            Object.defineProperty(obj, fullModuleName, {
                configurable: true,
                enumerable: true,
                get: function () {
                    Object.defineProperty(obj, fullModuleName, {
                        configurable: false,
                        enumerable: true,
                        value: mod
                    });
                    console.log('System API module "%s" defined', fullModuleName);
                    inspect('parent object', obj);
                    inspect(fullModuleName, obj[fullModuleName]);
                }
            });
        }
        if (!errFlag) {
            console.log('Callback for "%s"', fullModuleName);
            callback(obj[fullModuleName]);
        }
    };
     
    //установка модуля из репозитория
    //todo установка модуля в папку с версией name@version, ограничить версию major.minor для накатки fix
    var installModule = function(obj, repoURL, name, version, callback){
        console.log('Installing module %s version %s', name, version);
        var fullURL = getFullURL(repoURL, name, version);
        npm.load({progress: true, '--save-optional': true, '--force': true, '--ignore-scripts': true},function(err) {
            // handle errors
     
            // install module
            npm.commands.install([fullURL], function(er, data) {
                // log errors or data
                if (!er){
                    console.info('System API module "%s" installed', name);
                    //повторная попытка определения свойства
                    demandModuleProperty(obj, repoURL, name, version, callback);
                } else {
                    console.error('Error NPM Install', er.code, er);
                }
            });
     
            npm.on('log', function(message) {
                // log installation progress
                console.log('NPM logs:' + message);
            });
        });
    };

    Решение пока еще не реализовано, мы работаем над этим и обязательно расскажем о результатах в другой статье. А пока хотим спросить у вас, что вы думаете о выбранном подходе? Какие подводные камни нас ждут? Приглашаем всех принять участие в дискуссии в комментариях.
    Метки:
    Поделиться публикацией
    Комментарии 39
    • +8
      Статья интересная, но оформление кода картинками — не очень хорошая затея. Лучше текстом)
      • +3
        Красиво подсвеченный код картинкой как раз неплохо (если цели копировать нет), но вот jpeg и криво вырезанные скриншоты это ужасно, создают представление о «профессионализме» компании… =/
        • 0
          Чему вы удивляетесь, это же ЕФС:

          Epic
          Fail
          System

          Код в jpeg картинках — это феерический звездец!
          • 0
            А вы повторяетесь)
            Судя по профилю, вы горячий поклонник блога Программы ЕФС и активный пользователь Хабра.
            Пожалуйста, расскажите о своем опыте по тематике публикации, какие темы для вас наиболее интересны?
          • 0
            примеры кода можно посмотреть тут: периферийное API и работа с окнами в windows
            • 0
              Спасибо за комментарий, доработали
            • +3
              На localhost выставляется cервис, который доступен из JavaScript. От этого тоже решили отказаться, т.к. реализация более трудоемкая, требуется использовать веб-сервер либо писать свой.


              И в итоге использовали веб-сервер.
              Java отлично бежит из одной Jar с использованием Spring Boot или Vert.x.
              Почему тогда не .Net?
              • 0
                Веб сокеты, а это двухсторонний обмен, т.е. не совсем веб :)
                • 0
                  Обе названные мной технологии для Java умеют включать websocket как в голом виде, так и с прослойками вроде STOMP или sockJS при сравнимом объёме конфигурационного кода.
              • +3
                К сожалению кроме как поднять локально сервер для работы со сканерами дактилоскопии не смогли ничего придумать… Хотели тонкий клиент вместо толстого, а получился утолщенный.
                • 0

                  А можете чуть подробнее рассказать, как предполагается работать с устройствами, которыми сами инициируют события (а не отвечают на запрос JS-приложения в браузере)? Тот же сканер например.


                  И что будет, если пользователь откроет JS-приложение в нескольких вкладках браузера, а потом отсканирует штриход?

                  • +1
                    Там же веб сокеты, это не веб сервер. Так что ни с тем, ни с тем проблем не будет.
                    • 0
                      В этом случае будет во всех вкладках отклик.
                      • 0
                        И что будет, если пользователь откроет JS-приложение в нескольких вкладках браузера, а потом отсканирует штриход?


                        Я делаю так: страница загружает SharedWorker, который уже устанавливает WebSocket-соединение с драйвером. По событиям focus/blur на window, страница отправляет сообщения в воркер. Таким образом воркер знает, какой из страниц отправлять событие от драйвера. Плюс есть возможность реагировать на событие специальным образом, если ни одна из страниц не активна в данный момент.
                      • +1
                        Я бы electron заюзал, там уже внутри NodeJS.
                        • 0

                          Довольно очевидное решение, которое будет работать — почему бы и нет. Сам периодически такое пишу. Например, программа для прошивки MPOS терминалов написана именно на node.js — настройка в браузере, потом вызов локальной ноды, которая уже дёргает вендорское приложение или нативные мультиплатформенные модули. В плюсах — есть интерфейс, удобно работать, легко поддерживать, не надо размываться на ещё один стек. Минусов пока не обнаружено.


                          По вашему коду — непонятно, почему у вас в 2017 году древний javascript и callback hell как он есть.

                          • 0

                            рабочий прототип. JS при написании модулей можно заменить TypeScript для удобства разработчика, и уже из него генерировать JS

                          • +1
                            Что-то вы намудрили. В начале статьи казалось, что вы хотите обойтись чисто браузером, безо всяких устанавливаемых приложений, но в итоге пришли к запуску node.js на каждом компьютере, да еще и под серверным аккаунтом.
                            На вашем месте я бы написал приложение со своим HTTP API, на любом языке, для которого есть библиотеки для работы с нужным оборудованием. Да, примерно как сейчас, но без серверного яваскрипта, все-таки не думаю, что он подходит для работы с кучей разного оборудования :)
                            • 0
                              не рассматривали JWS (java web start)? написать свой сервер — вот пример blindscanner.com/ru, вполне прикольная штучка. устанавливается просто.
                              • 0
                                прошу прщения — не правильная ссылка была выложена, Вот правильная unit6.ru/twain-web. (это не раклама, а пример удачного решения, на мой взгляд) Решаемая задача — аналогичная, использование web для подключения к железу. Причём устанавливаемый «сервер» доступен не только локальному компу, но и по сети.
                              • 0
                                Как взаимодействуют Браузер и сервер ясно, но было бы интересно про взаимодействие сервера и самой периферии. Какие разъемы, протоколы итп.
                                • 0

                                  npm модуль ffi для вызова системной библиотеки — драйвера устройства или API ОС

                                • 0
                                  Стоило ли использовать socket.io в проекте? Мне кажется и без него вы могли реализовать WebSockets.
                                  Конечно оно проще заюзать эту либу, но смысл? Ни в браузере, ни на сервере оно не нужно. Реализовать можно и без этой «библиотеки для чата».
                                  И вообще: https://github.com/uNetworking/uWebSockets
                                  Пишите на C++/С. Не нужна будет node с серверным аккаунтом.
                                  • 0
                                    Вставлю свои пять копеек. Наверное самый главный аргумент против — это использование JS для работы со всем этим банковским зоопарком оборудования. Та же Java (на которой СберТех сейчас активно пишет, насколько я понимаю), или .NET, всяко лучше будут интегрироваться с этим железом. Получается что JS-ников у вас больше чем Java-истов:-) А так еще вот что хочется спросить — канал связи браузера с локальным WebSocket-сервером будет как-то защищаться (SSL)? Я голосую за Java и выкачивание актуальных модулей подключения к банковскому железу (JAR-ников) с внутреннего репозитория при каждой загрузке локальной среды исполнения перефирийного ПО (= включению компа).
                                    • 0
                                      А так еще вот что хочется спросить — канал связи браузера с локальным WebSocket-сервером будет как-то защищаться (SSL)?
                                      для этих целей существует wss/
                                      Я голосую за Java и выкачивание актуальных модулей подключения к банковскому железу (JAR-ников) с внутреннего репозитория при каждой загрузке локальной среды исполнения перефирийного ПО (= включению компа).
                                      технология называется JWS.
                                    • +1
                                      Банк, в отделениях которого работают более 100 тыс. операторов. У них есть рабочие места, на которых они обслуживают клиентов.

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

                                      • 0
                                        есть куча вопросов, которые можно решить только в офисе банка.
                                        • 0
                                          Роль офиса играет приложение на смартфоне/личный кабинет на сайте банка.

                                          Ребята из одного банка действительно постарались, чтобы всё было удобно.
                                          Пример: хочу открыть расчётный счёт для ИП. Захожу в красный банк: нужно предоставить аж 10 документов (я серьёзно, у них до сих пор висит пдфка с 11 пунктами). А в жёлтом просят только ИНН и паспортные данные, ОГРН курьер спрашивает когда привозит уже готовую карту. В итоге пользуюсь 5+ лет, потребность съездить в офис ни разу не возникала.
                                      • 0
                                        Тоже не пойму почему вместо HTTP врапера на с++ или GO к примеру вы выбрали ноду которую надо ставить на «более 100 тыс. операторов. „
                                        • 0
                                          А вы никогда не слышали про Electron?
                                          • 0
                                            WebUSB это только в хроме фича, или в стандарт / другие браузеры проберётся?
                                            Оно доступно везде, или только для расширений?

                                            Находила также, что есть что-то для работы с HID, но, вроде бы тоже только для хрома/расширений и только в экспериментальной версии.
                                            • 0
                                              Я делал через JS <-> socket.io <-> Windows Service (C#) Тоже хорошо. Надо было соединять с Bluetooth, USB, Ethernet через спец драйвера
                                              • 0

                                                Мы лично для себя не смогли придумать что-то более адекватного чем orange pi с сервером на борту для общения с переферией

                                                • 0
                                                  У меня давно периферия(сканеры штрихкода, весы, фискальные регистраторы) может через браузер работать используя для коммуникации websocket.
                                                  Который можно открыть как ws://localhost
                                                  И кроспплатформенный драйвер (на java или node ) читает локальные(а может и другого компа) rs-232 и «кричит в websocket»… и браузер обрабатывает.
                                                  • +1
                                                    Был опыт разработки чего-то подобного. Возможно не «подводный камень» но все же вставлю свои 5 копеек, которые немного нам подпортили крови в свое время:
                                                    В данном решении необходимо помнить, что после того, как вы начнете использовать https для веб-страницы (зачастую сайт переводят на https в последний момент перед релизом), вы не сможете делать вызовы на ваш node.js сервер, работающий по http из-за Mixed Content.
                                                    В свою очередь, т.к. вы совершаете https вызов из браузера на localhost, вам необходимо чтобы этот localhost имел валидный SSL сертификат issued to «localhost», выданный trusted Certificate authority. Понятное дело, что никакой Certificate authority такой сертификат не выдаст.
                                                    И тут начинаются костыли и выбор из меньших зол:
                                                    1. Можно сгенерировать self-signed certificate для Certificate authority. Потом подписать этим сертификатом SSL сертификат на localhost. Теперь у вас есть SSL сертификат на localhost, но при этом вам придется устанавливать self-signed certificate c certificate authority в trusted root сертификаты вашей системы. Уверен корпоративные безопасники это по достоинству оценят. Вот тут есть информация про скандал с Dell, который установил свои сертификаты в trusted root и чем это закончилось.
                                                    2. Можно купить валидный сертификат на некоторый домен например example.com. Использовать его в node.js сервере, при этом в hosts файле переписать example.com на localhost. Что тоже ужасно.
                                                    Будет интересно от Вас услышать, как решите эту проблему у себя. Конечно при условии что вы будете использовать https для своего сайта.
                                                    • 0
                                                      проблема существует, но если использовать websocket для связи с localhost, то всё сводится к установке самоподписанного сертификата для wss на локальный комп. Это несколько не камильфо, но для корпоративного использования можно. на всё остальное это не повлияет.
                                                      • 0
                                                        Решаю проблему модифицированным вторым способом. Есть сервер, который для localhost.example.com получает сертификат от LetsEncrypt и выкладывает его с ключом на HTTP-сервере. При запуске драйвер запрашивает новый сертификат и сохраняет его. В случае проблем с сервером, сохраненной копией можно будет пользоваться ещё 3 месяца. Остается также в hosts прописать localhost.example.com.
                                                      • 0
                                                        Предпосылка о переносимости изначально неверна, просто потому что драйвера непереносимы по определению. Соответственно, можно перестать заниматься придумыванием способа исполнения нативного кода через жепу из js и

                                                        1) под windows написать сервис на любом языке (напр. javase с оберткой), который локально под управлением svchost будет выполнять те же самые функции, но без извращений. Как бонус получаем централизованное обновление сервиса через ad (про что вы конечно же забыли). Под linux это на ура переносится на systemd, после того, естественно, когда все драйвера перепишутся. Про jws уже выше предлагали, это в принципе то же самое, только без управления.

                                                        или

                                                        2) запиливаются плагины под браузер (целевой в банке — ie11, а слова про линукс и хром — это, пардон, розовые мечты апологетов js) и все работает. Обновление плагинов осуществляется опять же централизованно через ad. У CryptoPro плагины, например, уже есть. Фраза «Это тоже нам не подходит, не является целевым, привязывает к браузеру и ограничивает универсальность.» является сомнительной демагогией.
                                                        • 0
                                                          1. Раскатка сейчас через microsoft sccm предполагается и подготовка пакета будет включать в себя драйвер и обвязку на JS (npm пакет модуля работы с периферией). Раскатка и поддержка самой JVM, не отличается от Node.JS. С Linux будет похожая история. Пока явных плюсов в написании модулей на Java не вижу.
                                                          2. От плагинов отказались намерено, о чем написали в начале статьи. Также были предложения писать свой браузер на основе готовых платформ…

                                                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                        Самое читаемое