Pull to refresh
442.56
Сбер
Больше чем банк

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

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

Статья будет полезна архитекторам и опытным 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);
        });
    });
};

Решение пока еще не реализовано, мы работаем над этим и обязательно расскажем о результатах в другой статье. А пока хотим спросить у вас, что вы думаете о выбранном подходе? Какие подводные камни нас ждут? Приглашаем всех принять участие в дискуссии в комментариях.
Tags:
Hubs:
+28
Comments 39
Comments Comments 39

Information

Website
www.sber.ru
Registered
Founded
Employees
over 10,000 employees
Location
Россия
Representative