Skip to content

系统交互

Node.js 做运维脚本时经常要读取环境变量、调用系统命令并处理退出码,后续再交给 systemd 或定时任务运行。

一、环境变量

配置从环境变量进入进程。

js
const port = Number(process.env.PORT || 3000);
const logLevel = process.env.LOG_LEVEL || "info";

console.log({ port, logLevel });

必填变量单独封装。

js
export function requireEnv(name) {
  const value = process.env[name];

  if (!value) {
    throw new Error(`${name} is required`); // 启动阶段缺配置直接失败
  }

  return value;
}

布尔值按明确值解析,不直接拿字符串真假来判断。

js
function readBooleanEnv(name, defaultValue = false) {
  const value = process.env[name];

  if (value === undefined) {
    return defaultValue;
  }

  return value === "true"; // 只有明确写 true 才当成 true
}

const dryRun = readBooleanEnv("DRY_RUN", false);
console.log({ dryRun });

"false" 是非空字符串,直接放进 if (process.env.DRY_RUN) 会被当成真。

二、execFile

execFile 适合执行明确的命令和参数,不经过 shell。

js
import { execFile } from "node:child_process";
import { promisify } from "node:util";

const execFileAsync = promisify(execFile);

const { stdout } = await execFileAsync("node", ["-v"]);
console.log(stdout.trim());

检查 systemd 服务状态:

js
import { execFile } from "node:child_process";
import { promisify } from "node:util";

const execFileAsync = promisify(execFile);

async function isActive(service) {
  try {
    const { stdout } = await execFileAsync("systemctl", ["is-active", service]);
    return stdout.trim() === "active"; // systemctl 输出 active/inactive/failed
  } catch {
    return false; // is-active 非 active 时退出码非 0,这里转成 false
  }
}

console.log(await isActive("nginx"));

外部传入的参数不拼进一整条 shell 字符串。用参数数组传,参数就是参数,也少一类命令注入问题。

三、spawn

需要实时看到输出时用 spawn

js
import { spawn } from "node:child_process";

const child = spawn("ping", ["-c", "4", "127.0.0.1"], {
  stdio: "inherit" // 子进程输出直接继承当前终端
});

child.on("exit", (code) => {
  process.exit(code ?? 1); // 把子进程退出码传给当前脚本
});

spawn 适合执行长时间命令,比如备份、同步、构建。

四、exec

exec 会经过 shell,适合确实需要管道、重定向、通配符的场景。

js
import { exec } from "node:child_process";
import { promisify } from "node:util";

const execAsync = promisify(exec);

const { stdout } = await execAsync("ps aux | grep nginx | head");
console.log(stdout);

用户输入不进 exec 字符串。

js
// 反例:service 来自参数时可能注入额外命令
// await execAsync(`systemctl restart ${service}`);

替代写法:

js
await execFileAsync("systemctl", ["restart", service]); // 参数作为独立元素传入

五、超时

命令执行要考虑卡住。

js
import { execFile } from "node:child_process";
import { promisify } from "node:util";

const execFileAsync = promisify(execFile);

await execFileAsync("curl", ["-fsS", "http://127.0.0.1:3000/healthz"], {
  timeout: 5000 // 超过 5 秒会终止子进程
});

curl 参数含义:

text
-f  HTTP 4xx/5xx 时返回失败退出码
-s  静默模式,不输出进度条
-S  静默时仍显示错误信息

六、标准输出格式

机器读的结果适合输出 JSON。内部工具早晚会被别的脚本接上,结构化数据能少写很多 awk/sed 文本拆分。

js
const result = {
  service: "nginx",
  active: true,
  checkedAt: new Date().toISOString()
};

console.log(JSON.stringify(result)); // 一行 JSON,方便日志采集和管道处理

给人看的表格可以用 TSV。

js
const rows = [
  ["service", "status"],
  ["nginx", "active"],
  ["sshd", "active"]
];

for (const row of rows) {
  console.log(row.join("\t")); // tab 分隔,终端和 awk 都好处理
}

七、systemd 服务

长期运行的 Node.js 服务可以交给 systemd。

ini
[Unit]
Description=Node Ops Service
After=network.target

[Service]
Type=simple
WorkingDirectory=/opt/node-ops
Environment=PORT=3000
ExecStart=/usr/local/node/bin/node server.js
Restart=always
RestartSec=5
User=nodeapp
Group=nodeapp

[Install]
WantedBy=multi-user.target

字段说明:

text
WorkingDirectory  进程启动后的工作目录,影响 process.cwd()
Environment       注入环境变量
ExecStart         启动命令,写绝对路径少踩 PATH 的坑
Restart           异常退出后自动拉起
User/Group        用低权限用户运行,避免默认 root

管理命令:

bash
systemctl daemon-reload
systemctl start node-ops
systemctl enable node-ops
journalctl -u node-ops -f

八、定时任务

简单脚本可以由 crontab 调度。

bash
*/5 * * * * /usr/local/bin/node /opt/check-service/index.js >>/var/log/check-service.log 2>&1

这里把 stdout 和 stderr 都追加到日志文件。正式环境我更偏向 systemd timer 或 Kubernetes CronJob,失败状态、重试和日志入口都好查。crontab 不是不能用,只是看上次有没有跑成会麻烦一点。

systemd oneshot:

ini
[Unit]
Description=Run service check

[Service]
Type=oneshot
WorkingDirectory=/opt/check-service
ExecStart=/usr/local/bin/node index.js

timer:

ini
[Unit]
Description=Run service check every 5 minutes

[Timer]
OnBootSec=1min
OnUnitActiveSec=5min
Unit=check-service.service

[Install]
WantedBy=timers.target

九、一个巡检脚本

配置文件:

json
{
  "services": ["nginx", "sshd", "crond"]
}

脚本:

js
import { readFile } from "node:fs/promises";
import { execFile } from "node:child_process";
import { promisify } from "node:util";

const execFileAsync = promisify(execFile);

async function isActive(service) {
  try {
    const { stdout } = await execFileAsync("systemctl", ["is-active", service], {
      timeout: 3000 // 单个服务检查最多等 3 秒
    });

    return stdout.trim() === "active";
  } catch {
    return false;
  }
}

async function main() {
  const text = await readFile("services.json", "utf8");
  const config = JSON.parse(text);
  const results = [];

  for (const service of config.services) {
    results.push({
      service,
      active: await isActive(service)
    });
  }

  console.log(JSON.stringify(results, null, 2));
}

main().catch((err) => {
  console.error(err.message);
  process.exit(1);
});