Статьи

Создание батареи, а именно с использованием Node.js: Начало работы и сервер

Если ваша первоначальная реакция на название этой статьи была что-то вроде ЧТО? Я хочу вас успокоить. Вы не должны поверить на мое слово! Я собираюсь показать вам, как создать прекрасное программное обеспечение, которое может работать на нескольких операционных системах, взаимодействовать с ними и представлять результаты в приятной форме. Вся задача будет достигнута с помощью JavaScript и небольшого количества команд bash / powershell.

Сказал, что вы можете быть удивлены, почему я хочу провести этот эксперимент. Это может быть неожиданностью, но «зимние ночи длинные и одинокие, и мне нужно было что-то убить», — это не ответ на этот вопрос. Возможно, что-то вроде «Я хотел усовершенствовать свои навыки и освоить JS» было бы ближе.

Хотя этот проект не несет в себе высокой ценности, мое скромное мнение таково:

  • предоставит вам навыки (и немного базового дизайна) для создания сервиса RESTful и любого интерфейса, который вы хотели бы для своей любимой ОС
  • позволяет сосредоточиться на совместимости между ОС
  • познакомить вас с ценными шаблонами проектирования для JavaScript и полезными модулями Node.js.

Имея это в виду, давайте начнем говорить о сервере. Нам нужно создать (RESTful) сервис, который в реальном времени предоставляет нам последние показания нашей ОС.

Зачем нам нужен сервер? А почему RESTful?

Ответ на эти два умных вопроса прост. Во-первых, нам нужен сервер, потому что по соображениям безопасности браузер не позволил бы вам выполнить команду в операционной системе (держу пари, что вы не будете слишком рады, если какой-нибудь жуткий веб-сайт сможет стереть все ваши файлы, не могли бы вы?). Во-вторых, у нас будет служба RESTful, поскольку в использовании интерфейсов REST есть несколько преимуществ. Это выходит за рамки нашей компетенции, но я укажу заинтересованным читателям на несколько хороших ресурсов, чтобы узнать больше об этой теме в конце этой статьи.

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

Чтобы отправить эти данные обратно, нам, безусловно, необходимо согласовать формат. Мы могли бы отправить некоторый необработанный текст и оставить анализ для клиента или, в качестве альтернативы, мы могли бы отправить структурированные данные (используя, например, XML). Я выбрал JSON. Причина в том, что мы будем иметь структурированные данные, но гораздо менее избыточные, чем XML. Обратите внимание, что, согласовав формат данных, мы вводим определенную связь для клиента, которая теперь должна соответствовать нашему форматированию. Тем не менее, этот выбор получает несколько преимуществ:

  • Мы можем указать формат как часть нашего интерфейса: клиенты, естественно, должны придерживаться API любого сервиса, который они используют (например, имя методов или предоставляемая конечная точка), и до тех пор, пока мы не изменим формат, не будет никакой разницы. Очевидно, что мы все еще должны продумать этот формат до того, как перейдем к версии 1. На самом деле, мы должны (почти) никогда не менять публичный интерфейс, чтобы избежать взлома клиентов.
  • Мы бы заметно замедляли работу клиентов, делегируя им разбор.
  • Мы получаем разделение от разных ОС, предоставляя общий формат для всех из них. Для поддержки новой ОС нам нужен только адаптер для данных, которые мы получаем от нее.

На этом этапе нам нужно начать разговор о том, как и где мы получим данные, которые мы отправляем клиенту. Это, пожалуй, самая сложная часть игры, но, к счастью, существует множество модулей для Node.js, которые позволяют нашему серверу взаимодействовать с нашей ОС и даже понимать, какая ОС работает на нашей машине.

Создание конечных точек

Чтобы создать ядро ​​нашего сервиса, нам нужно использовать HTTP-модуль Node.js для обработки входящих GET-запросов:

var http = require('http'); var PORT = 8080; 

Поскольку мы создаем приложение, которое будет работать только на локальном хосте, мы можем использовать статическое (постоянное) значение для порта. Другой вариант — прочитать его из командной строки и вернуться к постоянному значению, если это не предусмотрено. Мы можем прочитать аргументы командной строки из process.argv . Поскольку первый аргумент всегда будет "node" а второй — именем файла JavaScript, который мы запускаем, нас интересует третий аргумент:

 var PORT = Number(process.argv[2]) || 8080; 

Модуль HTTP позволяет легко создать сервер и прослушать порт. Нам просто нужно использовать две функции, объявленные в модуле, createServer() и listen() . Первый принимает в качестве входных данных обратный вызов с двумя аргументами, запросом и его ответом, а второй просто принимает номер порта, который нам нужно прослушать. Мы хотим создать конечные точки REST, поэтому нам нужно проверить, какой путь был запрошен. Более того, мы хотим выполнять разные действия в зависимости от того, с какой из наших конечных точек он совпадает. Допустим, мы хотим, чтобы путь для информации о /battery был /battery . Чтобы учесть небольшие вариации (например, /battery/ ), мы собираемся определить регулярное выражение для соответствия нашей конечной точке:

 var RE_BATTERY = /\/battery\/?/; 

Возвращаясь к аргументу createServer() , это будет функция, обеспечивающая доступ к объекту запроса (и ответа), который, в свою очередь, имеет поле с запрошенным URL-адресом. Собирая все это вместе, мы должны иметь следующий код:

 var server = http.createServer(function (request, response) { var requestUrl = request.url; if (RE_BATTERY.test(requestUrl)) { getBatteryStatus(response, onBatteryInfo, onError); } }).listen(PORT); 

getBatteryStatus() — это функция, которую мы вскоре определим. Мы делегируем этой функции ответственность за отправку ответа вызывающей стороне с помощью двух методов response : write() и end() .

Обслуживание статического контента

Помимо определения нашей конечной (ых) точки (ей), нам также необходимо предоставить некоторый статический контент, который будет предоставлен тем же сервером. Различный дизайн с двумя серверами, один для статического контента и один для динамического контента, также был бы возможен. Тем не менее, это может быть ненужным, если не вредным, поскольку нет необходимости занимать еще один порт, если мы считаем, что мы будем единственным клиентом, запрашивающим статическое содержимое.

Модуль HTTP приходит на помощь даже в этом случае. Во-первых, если клиенты запрашивают наш root , мы перенаправим их на нашу главную страницу:

 if (requestUrl === '/' || requestUrl === '') { response.writeHead(301, { Location: BASE_URL + 'public/demo.html' }); response.end(); } else if (RE_BATTERY.test(requestUrl)) { getBatteryStatus(response, onBatteryInfo, onError); } 

Затем мы добавим ветку `else` к условному условию выше. Если запрос не совпадает ни с одной из наших конечных точек, наш сервер проверит, существует ли статический файл для этого пути, и обработает его, или ответит HTTP-кодом 404 (не найден).

 else { fs.exists(filePath, function (exists) { if (exists) { fs.readFile(filePath, function (error, content) { if (error) { response.writeHead(500); response.end(); } else { response.writeHead(200); response.end(content, 'utf-8'); } }); } else { response.writeHead(404, {'Content-Type': 'text/plain'}); response.write('404 - Resurce Not found'); response.end(); } }); } 

Запуск команд ОС

Для запуска команд нашей операционной системы из Node.js нам нужен еще один модуль под названием child_process , который также предоставит нам несколько служебных методов.

 var child_process = require('child_process'); 

В частности, мы собираемся использовать метод exec (), который позволяет запускать команды в оболочке и буферизировать их вывод.

 child_process.exec("command", function callback(err, stdout, stderr) { //.... }); 

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

Определить текущую ОС

Node.js предоставляет простой способ проверки базовой ОС. Нам нужно проверить process.platform и включить его значение (будьте осторожны с особенностями именования):

 function switchConfigForCurrentOS () { switch(process.platform) { case 'linux': //... break; case 'darwin': //MAC //... break; case 'win32': //... break; default: //... } } 

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

OSX
 pmset -g batt | egrep "([0-9]+\%).*" -o 
Linux
 upower -i /org/freedesktop/UPower/devices/battery_BAT0 | grep -E "state|time to empty|to full|percentage" 
Windows
 wmic Path Win32_Battery 

Применение шаблона шаблона — OS-зависимый дизайн

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

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

Хотя некоторые детали меняются, общий рабочий процесс для обработки запросов будет одинаковым во всех ОС:

  1. Мы вызываем child_process.exec для запуска команды;
  2. Мы проверяем, была ли команда успешно выполнена, в противном случае мы имеем дело с ошибкой;
  3. Предполагая, что все прошло успешно, мы обрабатываем вывод команды, извлекая необходимую нам информацию;
  4. Мы создаем ответ и отправляем его обратно клиенту.

Это идеальное применение для Template method design pattern описанного в « Банде четырех книг» .

Поскольку JavaScript на самом деле не ориентирован на классы, мы реализуем вариант шаблона, в котором детали, а не подклассы, передаются функциям, которые будут «переопределены» (через присваивание), в зависимости от текущей ОС.

 function getBatteryStatus(response, onSuccess, onError) { child_process.exec(CONFIG.command, function execBatteryCommand(err, stdout, stderr) { var battery; if (err) { console.log('child_process failed with error code: ' + err.code); onError(response, BATTERY_ERROR_MESSAGE); } else { try { battery = CONFIG.processFunction(stdout); onSuccess(response, JSON.stringify(battery)); } catch (e) { console.log(e); onError(response, BATTERY_ERROR_MESSAGE); } } }); } 
команды

Теперь мы можем добавить то, что мы уже узнали о командах, в нашу switchConfigForCurrentOS() . Как упомянуто выше, нам нужно переопределить как запуск команды, так и функцию постобработки, в соответствии с текущей ОС.

 function switchConfigForCurrentOS() { switch (process.platform) { case 'linux': return { command: 'upower -i /org/freedesktop/UPower/devices/battery_BAT0 | grep -E "state|time to empty|to full|percentage"', processFunction: processBatteryStdoutForLinux }; case 'darwin': //MAC return { command: 'pmset -g batt | egrep "([0-9]+\%).*" -o', processFunction: processBatteryStdoutForMac }; case 'win32': return { command: 'WMIC Path Win32_Battery', processFunction: processBatteryStdoutForWindows }; default: return { command: '', processFunction: function () {} }; } } 
Обработка Bash Output

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

Альтернативой может быть отправка дополнительного параметра "OS" клиенту, но я думаю, что соединение введено. Более того, разделение логики между сервером (там, где он принадлежит) и клиентом было бы большим отключением, чем любое возможное упрощение или повышение производительности.

 function processLineForLinux(battery, line) { var key; var val; line = line.trim(); if (line.length > 0) { line = line.split(':'); if (line.length === 2) { line = line.map(trimParam); key = line[0]; val = line[1]; battery[key] = val; } } return battery; } function mapKeysForLinux(battery) { var mappedBattery = {}; mappedBattery.percentage = battery.percentage; mappedBattery.state = battery.state; mappedBattery.timeToEmpty = battery['time to empty']; return mappedBattery; } function mapKeysForMac(battery) { var mappedBattery = {}; mappedBattery.percentage = battery[0]; mappedBattery.state = battery[1]; mappedBattery.timeToEmpty = battery[2]; return mappedBattery; } function processBatteryStdoutForLinux(stdout) { var battery = {}, processLine = processLineForLinux.bind(null, battery); stdout.split('\n').forEach(processLine); return mapKeysForLinux(battery); } function processBatteryStdoutForMac(stdout) { var battery = stdout.split(';').map(trimParam); return mapKeysForMac(battery); } 

Функции обработки для Windows немного сложнее, и для простоты они опущены в этом контексте.

Собираем все вместе

На данный момент нам просто нужно выполнить некоторую разводку, кодировать наши данные в JSON и несколько констант, которые нам еще нужно объявить. Вы можете взглянуть на окончательный код сервера на GitHub .

Выводы

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

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