import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import test from 'node:test'; import { createSingboxRuntime } from '../../src/server/singboxRuntime.js'; async function waitForStarts(filePath, count) { for (let attempt = 0; attempt < 100; attempt += 1) { if (fs.existsSync(filePath) && fs.readFileSync(filePath, 'utf8').length >= count) return; await new Promise((resolve) => setTimeout(resolve, 10)); } throw new Error(`sing-box did not start ${count} time(s)`); } test('dataplane keeps sing-box running when the applied config is unchanged', async (t) => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-dataplane-')); const binDir = path.join(dir, 'bin'); const configPath = path.join(dir, 'config.json'); const startsPath = path.join(dir, 'starts'); fs.mkdirSync(binDir); fs.writeFileSync(configPath, '{}'); fs.writeFileSync(path.join(binDir, 'sing-box'), `#!/usr/bin/env node if (process.argv[2] === 'check') process.exit(0); require('node:fs').appendFileSync(process.env.SINGBOX_TEST_STARTS, 'x'); process.on('SIGTERM', () => process.exit(0)); setInterval(() => {}, 60_000); `); fs.chmodSync(path.join(binDir, 'sing-box'), 0o755); const previousPath = process.env.PATH; process.env.PATH = `${binDir}:${previousPath}`; process.env.SINGBOX_TEST_STARTS = startsPath; const runtime = createSingboxRuntime({ configPath }); t.after(async () => { await runtime.stop(); process.env.PATH = previousPath; delete process.env.SINGBOX_TEST_STARTS; fs.rmSync(dir, { recursive: true, force: true }); }); await runtime.apply(); await waitForStarts(startsPath, 1); await runtime.apply(); assert.equal(fs.readFileSync(startsPath, 'utf8'), 'x'); fs.writeFileSync(configPath, '{"changed":true}'); await runtime.apply(); await waitForStarts(startsPath, 2); assert.equal(fs.readFileSync(startsPath, 'utf8'), 'xx'); });