Когда JavaScript выходит за пределы браузера
Большинство разработчиков привыкли воспринимать JavaScript как язык для «оживления» веб-страниц. Но что, если я скажу вам, что на JavaScript можно писать скрипты для управления операционной системой, работать с файловой системой на низком уровне и даже создавать сетевые демоны? Звучит как фантастика? А вот и нет. Благодаря средам выполнения вроде Node.js и Deno, JavaScript для системного программирования стал не просто возможным, а вполне эффективным инструментом. В этой статье мы разберем реальные примеры кода и посмотрим, как JS справляется с задачами, которые раньше считались прерогативой C, Rust или Python.
Почему JavaScript? Преимущества и подводные камни
Прежде чем нырять в код, давайте честно оценим ситуацию. Node.js системное программирование имеет свои сильные и слабые стороны.
Плюсы:
- Единый стек технологий — если вы фронтенд-разработчик, вам не нужно учить новый язык для написания утилит командной строки.
- Огромное сообщество — npm предлагает тысячи пакетов для работы с системой.
- Асинхронность из коробки — неблокирующий ввод-вывод идеален для работы с сетью и файлами.
- Быстрый прототипинг — написать скрипт на JS можно за считанные минуты.
Минусы:
- Производительность — JS медленнее C или Rust в вычислительных задачах.
- Ограниченный доступ к железу — прямой работы с памятью или портами ввода-вывода нет (хотя есть обходные пути).
- Потребление памяти — V8 не самый легковесный движок.
Тем не менее, для 80% задач системного администрирования и автоматизации JavaScript для системного программирования подходит отлично. Давайте перейдем к практике.
Работа с файловой системой: модуль fs
Начнем с основ — работа с файлами JS. Модуль fs в Node.js предоставляет как синхронные, так и асинхронные методы для взаимодействия с файловой системой.
Чтение и запись файлов (асинхронный вариант)
const fs = require('fs').promises;
const path = require('path');
async function processLogFile(filePath) {
try {
// Читаем файл
const data = await fs.readFile(filePath, 'utf-8');
const lines = data.split('\n');
// Фильтруем только строки с ошибками
const errors = lines.filter(line => line.includes('ERROR'));
// Записываем результат
const outputPath = path.join(__dirname, 'errors.log');
await fs.writeFile(outputPath, errors.join('\n'), 'utf-8');
console.log(`Обработано ${lines.length} строк, найдено ${errors.length} ошибок`);
} catch (err) {
console.error('Ошибка при работе с файлом:', err);
}
}
processLogFile('/var/log/application.log');
Этот скрипт — типичный пример системного скрипта на JavaScript. Он читает большой лог-файл, фильтрует данные и записывает результат. Асинхронная версия не блокирует event loop, что критично для серверных приложений.
Мониторинг изменений в файловой системе
Одна из мощных возможностей — слежение за файлами в реальном времени:
const fs = require('fs');
const watcher = fs.watch('/etc/config', { recursive: true });
watcher.on('change', (eventType, filename) => {
console.log(`Изменение в файле: ${filename} (${eventType})`);
// Автоматически перезагружаем конфигурацию
if (filename.endsWith('.conf')) {
const config = fs.readFileSync(`/etc/config/${filename}`, 'utf-8');
console.log('Новая конфигурация загружена');
}
});
console.log('Мониторинг конфигураций запущен...');
Это уже низкоуровневое программирование на JavaScript в действии — мы взаимодействуем с системными событиями inotify (на Linux) через абстракцию Node.js.
Управление процессами: child_process
Настоящая мощь Node.js системного программирования раскрывается при работе с дочерними процессами. Вы можете запускать любые системные команды, обрабатывать их вывод и даже передавать данные через pipe.
Запуск внешней команды и получение вывода
const { exec } = require('child_process');
const util = require('util');
const execPromise = util.promisify(exec);
async function getSystemInfo() {
try {
// Получаем информацию о дисках
const { stdout: dfOutput } = await execPromise('df -h');
console.log('Информация о дисках:');
console.log(dfOutput);
// Получаем список процессов
const { stdout: psOutput } = await execPromise('ps aux --sort=-%mem | head -10');
console.log('Топ процессов по памяти:');
console.log(psOutput);
// Сетевая статистика
const { stdout: netOutput } = await execPromise('ss -tuln');
console.log('Открытые порты:');
console.log(netOutput);
} catch (error) {
console.error('Ошибка выполнения команды:', error.stderr);
}
}
getSystemInfo();
Обратите внимание: мы используем promisify, чтобы избежать callback hell. Это стандартная практика для JS для администрирования.
Создание фонового процесса с pipe
const { spawn } = require('child_process');
function monitorCPULoad() {
// Запускаем top в пакетном режиме
const top = spawn('top', ['-b', '-n', '1']);
const grep = spawn('grep', ['Cpu(s)']);
top.stdout.pipe(grep.stdin);
let result = '';
grep.stdout.on('data', (data) => {
result += data.toString();
});
grep.on('close', (code) => {
if (code === 0) {
const cpuLoad = result.match(/(\d+\.\d+)%? id/);
if (cpuLoad) {
const idle = parseFloat(cpuLoad[1]);
const used = 100 - idle;
console.log(`Загрузка CPU: ${used.toFixed(1)}%`);
if (used > 80) {
console.warn('Внимание! Высокая загрузка CPU!');
}
}
}
});
}
// Запускаем мониторинг каждые 5 секунд
setInterval(monitorCPULoad, 5000);
Этот пример демонстрирует, как JavaScript для системного программирования может заменить классические bash-скрипты. Мы используем pipe между процессами точно так же, как в Unix shell, но с дополнительными возможностями логики на JS.
Сетевое программирование: создание простого монитора
Сеть — еще одна область, где системное ПО на JavaScript чувствует себя как рыба в воде. Встроенный модуль net позволяет создавать TCP-клиенты и серверы.