portok 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/bench/baseline.bench.js +1 -1
- package/bench/connections.bench.js +1 -1
- package/bench/helpers.js +124 -0
- package/bench/latency.bench.js +1 -1
- package/bench/run.js +0 -86
- package/bench/switching.bench.js +1 -1
- package/bench/throughput.bench.js +1 -1
- package/docker-compose.yml +1 -1
- package/package.json +1 -1
- package/portok.js +187 -2
- package/portok@.service +1 -1
- package/test/drain.test.js +27 -15
package/README.md
CHANGED
|
@@ -197,6 +197,7 @@ portok <command> [options]
|
|
|
197
197
|
Management Commands:
|
|
198
198
|
init Initialize portok directories (/etc/portok, /var/lib/portok)
|
|
199
199
|
add <name> Create a new service instance
|
|
200
|
+
remove <name> Remove a service instance (stops, disables, deletes config/state)
|
|
200
201
|
list List all configured instances and their status
|
|
201
202
|
|
|
202
203
|
Service Control Commands:
|
|
@@ -226,6 +227,10 @@ Options for 'add' command:
|
|
|
226
227
|
--health <path> Health check path (default: /health)
|
|
227
228
|
--force Overwrite existing config
|
|
228
229
|
|
|
230
|
+
Options for 'remove' command:
|
|
231
|
+
--force Skip confirmation prompt
|
|
232
|
+
--keep-state Keep state file (/var/lib/portok/<name>.json)
|
|
233
|
+
|
|
229
234
|
Options for 'logs' command:
|
|
230
235
|
--follow, -f Follow log output
|
|
231
236
|
--lines, -n Number of lines to show (default: 50)
|
|
@@ -275,6 +280,10 @@ sudo portok restart api
|
|
|
275
280
|
sudo portok enable api # Enable at boot
|
|
276
281
|
sudo portok disable api # Disable at boot
|
|
277
282
|
|
|
283
|
+
# Remove a service (stops, disables, removes config and state)
|
|
284
|
+
sudo portok remove api --force
|
|
285
|
+
sudo portok remove api --force --keep-state # Keep state file
|
|
286
|
+
|
|
278
287
|
# View logs
|
|
279
288
|
portok logs api
|
|
280
289
|
portok logs api --follow # Follow log output
|
package/bench/baseline.bench.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const autocannon = require('autocannon');
|
|
7
|
-
const { createMockServer, startDaemon, getFreePort, formatNumber } = require('./
|
|
7
|
+
const { createMockServer, startDaemon, getFreePort, formatNumber } = require('./helpers.js');
|
|
8
8
|
|
|
9
9
|
async function run({ duration, adminToken }) {
|
|
10
10
|
const shortDuration = Math.max(2, Math.floor(duration / 2));
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const autocannon = require('autocannon');
|
|
7
|
-
const { createMockServer, startDaemon, getFreePort, formatNumber } = require('./
|
|
7
|
+
const { createMockServer, startDaemon, getFreePort, formatNumber } = require('./helpers.js');
|
|
8
8
|
|
|
9
9
|
async function run({ duration, adminToken }) {
|
|
10
10
|
// Setup
|
package/bench/helpers.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Benchmark Helpers
|
|
3
|
+
* Shared utilities for all benchmark files
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { spawn } = require('node:child_process');
|
|
7
|
+
const http = require('node:http');
|
|
8
|
+
const path = require('node:path');
|
|
9
|
+
|
|
10
|
+
const ADMIN_TOKEN = process.env.ADMIN_TOKEN || 'bench-token-12345';
|
|
11
|
+
|
|
12
|
+
function formatNumber(n) {
|
|
13
|
+
return n.toLocaleString('en-US', { maximumFractionDigits: 2 });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function getFreePort() {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const server = http.createServer();
|
|
19
|
+
server.listen(0, '127.0.0.1', () => {
|
|
20
|
+
const port = server.address().port;
|
|
21
|
+
server.close(() => resolve(port));
|
|
22
|
+
});
|
|
23
|
+
server.on('error', reject);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function waitFor(condition, timeout = 10000) {
|
|
28
|
+
const start = Date.now();
|
|
29
|
+
while (Date.now() - start < timeout) {
|
|
30
|
+
if (await condition()) return true;
|
|
31
|
+
await new Promise(r => setTimeout(r, 100));
|
|
32
|
+
}
|
|
33
|
+
throw new Error('Timeout waiting for condition');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// =============================================================================
|
|
37
|
+
// Mock Server
|
|
38
|
+
// =============================================================================
|
|
39
|
+
|
|
40
|
+
async function createMockServer(options = {}) {
|
|
41
|
+
const { responseDelay = 0, responseSize = 13 } = options;
|
|
42
|
+
const port = await getFreePort();
|
|
43
|
+
|
|
44
|
+
const responseBody = responseSize <= 13
|
|
45
|
+
? 'Hello, World!'
|
|
46
|
+
: 'x'.repeat(responseSize);
|
|
47
|
+
|
|
48
|
+
const server = http.createServer((req, res) => {
|
|
49
|
+
if (req.url === '/health') {
|
|
50
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
51
|
+
res.end('{"status":"ok"}');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (responseDelay > 0) {
|
|
56
|
+
setTimeout(() => {
|
|
57
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
58
|
+
res.end(responseBody);
|
|
59
|
+
}, responseDelay);
|
|
60
|
+
} else {
|
|
61
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
62
|
+
res.end(responseBody);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
server.listen(port, '127.0.0.1', () => {
|
|
68
|
+
resolve({
|
|
69
|
+
port,
|
|
70
|
+
close: () => new Promise(r => server.close(r)),
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
server.on('error', reject);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// =============================================================================
|
|
78
|
+
// Daemon Starter
|
|
79
|
+
// =============================================================================
|
|
80
|
+
|
|
81
|
+
async function startDaemon(proxyPort, targetPort, extraEnv = {}) {
|
|
82
|
+
const daemonPath = path.join(__dirname, '..', 'portokd.js');
|
|
83
|
+
|
|
84
|
+
const env = {
|
|
85
|
+
...process.env,
|
|
86
|
+
LISTEN_PORT: String(proxyPort),
|
|
87
|
+
INITIAL_TARGET_PORT: String(targetPort),
|
|
88
|
+
ADMIN_TOKEN,
|
|
89
|
+
DRAIN_MS: '100',
|
|
90
|
+
ROLLBACK_WINDOW_MS: '1000',
|
|
91
|
+
ROLLBACK_CHECK_EVERY_MS: '100',
|
|
92
|
+
ROLLBACK_FAIL_THRESHOLD: '2',
|
|
93
|
+
...extraEnv,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const daemon = spawn('node', [daemonPath], {
|
|
97
|
+
env,
|
|
98
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Wait for daemon to be ready
|
|
102
|
+
await waitFor(async () => {
|
|
103
|
+
try {
|
|
104
|
+
const res = await fetch(`http://127.0.0.1:${proxyPort}/__status`, {
|
|
105
|
+
headers: { 'x-admin-token': ADMIN_TOKEN },
|
|
106
|
+
});
|
|
107
|
+
return res.ok;
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}, 10000);
|
|
112
|
+
|
|
113
|
+
return daemon;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = {
|
|
117
|
+
createMockServer,
|
|
118
|
+
startDaemon,
|
|
119
|
+
getFreePort,
|
|
120
|
+
waitFor,
|
|
121
|
+
formatNumber,
|
|
122
|
+
ADMIN_TOKEN,
|
|
123
|
+
};
|
|
124
|
+
|
package/bench/latency.bench.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const autocannon = require('autocannon');
|
|
7
|
-
const { createMockServer, startDaemon, getFreePort } = require('./
|
|
7
|
+
const { createMockServer, startDaemon, getFreePort } = require('./helpers.js');
|
|
8
8
|
|
|
9
9
|
async function run({ duration, adminToken }) {
|
|
10
10
|
// Setup
|
package/bench/run.js
CHANGED
|
@@ -5,9 +5,6 @@
|
|
|
5
5
|
* Orchestrates all benchmarks and outputs formatted results
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
const { spawn } = require('node:child_process');
|
|
9
|
-
const http = require('node:http');
|
|
10
|
-
|
|
11
8
|
// =============================================================================
|
|
12
9
|
// Configuration
|
|
13
10
|
// =============================================================================
|
|
@@ -31,86 +28,6 @@ function log(msg) {
|
|
|
31
28
|
}
|
|
32
29
|
}
|
|
33
30
|
|
|
34
|
-
function formatNumber(n) {
|
|
35
|
-
return n.toLocaleString('en-US', { maximumFractionDigits: 2 });
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async function getFreePort() {
|
|
39
|
-
return new Promise((resolve, reject) => {
|
|
40
|
-
const server = http.createServer();
|
|
41
|
-
server.listen(0, '127.0.0.1', () => {
|
|
42
|
-
const port = server.address().port;
|
|
43
|
-
server.close(() => resolve(port));
|
|
44
|
-
});
|
|
45
|
-
server.on('error', reject);
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function waitFor(condition, timeout = 10000) {
|
|
50
|
-
const start = Date.now();
|
|
51
|
-
while (Date.now() - start < timeout) {
|
|
52
|
-
if (await condition()) return true;
|
|
53
|
-
await new Promise(r => setTimeout(r, 100));
|
|
54
|
-
}
|
|
55
|
-
throw new Error('Timeout waiting for condition');
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// =============================================================================
|
|
59
|
-
// Mock Server
|
|
60
|
-
// =============================================================================
|
|
61
|
-
|
|
62
|
-
async function createMockServer(port = 0) {
|
|
63
|
-
const server = http.createServer((req, res) => {
|
|
64
|
-
if (req.url === '/health') {
|
|
65
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
66
|
-
res.end('{"status":"healthy"}');
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
70
|
-
res.end('OK');
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
await new Promise(resolve => server.listen(port, '127.0.0.1', resolve));
|
|
74
|
-
return {
|
|
75
|
-
port: server.address().port,
|
|
76
|
-
close: () => new Promise(resolve => server.close(resolve)),
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// =============================================================================
|
|
81
|
-
// Start Daemon
|
|
82
|
-
// =============================================================================
|
|
83
|
-
|
|
84
|
-
async function startDaemon(listenPort, targetPort) {
|
|
85
|
-
const proc = spawn('node', ['portokd.js'], {
|
|
86
|
-
env: {
|
|
87
|
-
...process.env,
|
|
88
|
-
LISTEN_PORT: String(listenPort),
|
|
89
|
-
INITIAL_TARGET_PORT: String(targetPort),
|
|
90
|
-
ADMIN_TOKEN,
|
|
91
|
-
STATE_FILE: `/tmp/portok-bench-${Date.now()}.json`,
|
|
92
|
-
DRAIN_MS: '1000',
|
|
93
|
-
ROLLBACK_WINDOW_MS: '60000',
|
|
94
|
-
ROLLBACK_CHECK_EVERY_MS: '5000',
|
|
95
|
-
ROLLBACK_FAIL_THRESHOLD: '3',
|
|
96
|
-
},
|
|
97
|
-
stdio: 'pipe',
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
await waitFor(async () => {
|
|
101
|
-
try {
|
|
102
|
-
const res = await fetch(`http://127.0.0.1:${listenPort}/__status`, {
|
|
103
|
-
headers: { 'x-admin-token': ADMIN_TOKEN },
|
|
104
|
-
});
|
|
105
|
-
return res.ok;
|
|
106
|
-
} catch {
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
return proc;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
31
|
// =============================================================================
|
|
115
32
|
// Run Individual Benchmarks
|
|
116
33
|
// =============================================================================
|
|
@@ -206,6 +123,3 @@ main().catch(err => {
|
|
|
206
123
|
console.error('Benchmark failed:', err);
|
|
207
124
|
process.exit(1);
|
|
208
125
|
});
|
|
209
|
-
|
|
210
|
-
module.exports = { createMockServer, startDaemon, getFreePort, waitFor, formatNumber, ADMIN_TOKEN };
|
|
211
|
-
|
package/bench/switching.bench.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const autocannon = require('autocannon');
|
|
7
|
-
const { createMockServer, startDaemon, getFreePort, formatNumber, ADMIN_TOKEN } = require('./
|
|
7
|
+
const { createMockServer, startDaemon, getFreePort, formatNumber, ADMIN_TOKEN } = require('./helpers.js');
|
|
8
8
|
|
|
9
9
|
async function run({ duration, adminToken }) {
|
|
10
10
|
// Setup two servers
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const autocannon = require('autocannon');
|
|
7
|
-
const { createMockServer, startDaemon, getFreePort, formatNumber } = require('./
|
|
7
|
+
const { createMockServer, startDaemon, getFreePort, formatNumber } = require('./helpers.js');
|
|
8
8
|
|
|
9
9
|
async function run({ duration, adminToken }) {
|
|
10
10
|
// Setup
|
package/docker-compose.yml
CHANGED
package/package.json
CHANGED
package/portok.js
CHANGED
|
@@ -314,6 +314,8 @@ async function cmdSwitch(baseUrl, token, port, options) {
|
|
|
314
314
|
async function cmdInit(options) {
|
|
315
315
|
const configDir = ENV_FILE_DIR;
|
|
316
316
|
const stateDir = '/var/lib/portok';
|
|
317
|
+
const systemdDir = '/etc/systemd/system';
|
|
318
|
+
const serviceFileName = 'portok@.service';
|
|
317
319
|
|
|
318
320
|
console.log(`${colors.bold}Initializing Portok...${colors.reset}\n`);
|
|
319
321
|
|
|
@@ -351,6 +353,51 @@ async function cmdInit(options) {
|
|
|
351
353
|
results.push({ path: stateDir, status: 'error', error: err.message });
|
|
352
354
|
}
|
|
353
355
|
|
|
356
|
+
// Install systemd template unit
|
|
357
|
+
const systemdDest = path.join(systemdDir, serviceFileName);
|
|
358
|
+
try {
|
|
359
|
+
// Find the service file relative to this script
|
|
360
|
+
const scriptDir = path.dirname(process.argv[1]);
|
|
361
|
+
const possiblePaths = [
|
|
362
|
+
path.join(scriptDir, serviceFileName),
|
|
363
|
+
path.join(scriptDir, '..', serviceFileName),
|
|
364
|
+
'/opt/portok/' + serviceFileName,
|
|
365
|
+
];
|
|
366
|
+
|
|
367
|
+
let sourceFile = null;
|
|
368
|
+
for (const p of possiblePaths) {
|
|
369
|
+
if (fs.existsSync(p)) {
|
|
370
|
+
sourceFile = p;
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (sourceFile) {
|
|
376
|
+
const content = fs.readFileSync(sourceFile, 'utf-8');
|
|
377
|
+
const existingContent = fs.existsSync(systemdDest) ? fs.readFileSync(systemdDest, 'utf-8') : null;
|
|
378
|
+
|
|
379
|
+
if (existingContent === content) {
|
|
380
|
+
results.push({ path: systemdDest, status: 'exists' });
|
|
381
|
+
} else {
|
|
382
|
+
fs.writeFileSync(systemdDest, content, { mode: 0o644 });
|
|
383
|
+
results.push({ path: systemdDest, status: 'created' });
|
|
384
|
+
|
|
385
|
+
// Reload systemd daemon
|
|
386
|
+
try {
|
|
387
|
+
const { execSync } = require('child_process');
|
|
388
|
+
execSync('systemctl daemon-reload', { stdio: 'pipe' });
|
|
389
|
+
results.push({ path: 'systemctl daemon-reload', status: 'created' });
|
|
390
|
+
} catch (e) {
|
|
391
|
+
// Ignore if systemctl not available (e.g., in Docker)
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
} else {
|
|
395
|
+
results.push({ path: systemdDest, status: 'skipped', error: 'Template file not found' });
|
|
396
|
+
}
|
|
397
|
+
} catch (err) {
|
|
398
|
+
results.push({ path: systemdDest, status: 'error', error: err.message });
|
|
399
|
+
}
|
|
400
|
+
|
|
354
401
|
if (options.json) {
|
|
355
402
|
console.log(JSON.stringify({ success: true, results }, null, 2));
|
|
356
403
|
return 0;
|
|
@@ -360,9 +407,11 @@ async function cmdInit(options) {
|
|
|
360
407
|
for (const r of results) {
|
|
361
408
|
const icon = r.status === 'created' ? `${colors.green}✓${colors.reset}` :
|
|
362
409
|
r.status === 'exists' ? `${colors.dim}○${colors.reset}` :
|
|
410
|
+
r.status === 'skipped' ? `${colors.yellow}○${colors.reset}` :
|
|
363
411
|
`${colors.red}✗${colors.reset}`;
|
|
364
412
|
const status = r.status === 'created' ? 'Created' :
|
|
365
413
|
r.status === 'exists' ? 'Already exists' :
|
|
414
|
+
r.status === 'skipped' ? `Skipped: ${r.error}` :
|
|
366
415
|
`Error: ${r.error}`;
|
|
367
416
|
console.log(` ${icon} ${r.path} - ${status}`);
|
|
368
417
|
}
|
|
@@ -377,7 +426,8 @@ async function cmdInit(options) {
|
|
|
377
426
|
console.log(`\n${colors.green}Portok initialized successfully!${colors.reset}`);
|
|
378
427
|
console.log(`\nNext steps:`);
|
|
379
428
|
console.log(` 1. Create a service: ${colors.cyan}portok add <name>${colors.reset}`);
|
|
380
|
-
console.log(` 2.
|
|
429
|
+
console.log(` 2. Start service: ${colors.cyan}portok start <name>${colors.reset}`);
|
|
430
|
+
console.log(` 3. Enable at boot: ${colors.cyan}portok enable <name>${colors.reset}`);
|
|
381
431
|
|
|
382
432
|
return 0;
|
|
383
433
|
}
|
|
@@ -495,6 +545,130 @@ ROLLBACK_FAIL_THRESHOLD=3
|
|
|
495
545
|
return 0;
|
|
496
546
|
}
|
|
497
547
|
|
|
548
|
+
async function cmdRemove(name, options) {
|
|
549
|
+
if (!name) {
|
|
550
|
+
console.error(`${colors.red}Error:${colors.reset} Service name is required`);
|
|
551
|
+
console.error('Usage: portok remove <name> [--force] [--keep-state]');
|
|
552
|
+
return 1;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const envFilePath = path.join(ENV_FILE_DIR, `${name}.env`);
|
|
556
|
+
const stateFilePath = `/var/lib/portok/${name}.json`;
|
|
557
|
+
|
|
558
|
+
// Check if service exists
|
|
559
|
+
if (!fs.existsSync(envFilePath)) {
|
|
560
|
+
console.error(`${colors.red}Error:${colors.reset} Service '${name}' not found at ${envFilePath}`);
|
|
561
|
+
return 1;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Confirmation (unless --force)
|
|
565
|
+
if (!options.force && !options.json) {
|
|
566
|
+
console.log(`${colors.yellow}Warning:${colors.reset} This will remove service '${name}'`);
|
|
567
|
+
console.log(` Config file: ${envFilePath}`);
|
|
568
|
+
if (fs.existsSync(stateFilePath) && !options['keep-state']) {
|
|
569
|
+
console.log(` State file: ${stateFilePath}`);
|
|
570
|
+
}
|
|
571
|
+
console.log(`\nUse ${colors.cyan}--force${colors.reset} to confirm removal.`);
|
|
572
|
+
return 1;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const results = [];
|
|
576
|
+
const { execSync } = require('child_process');
|
|
577
|
+
|
|
578
|
+
// 1. Stop the service if running
|
|
579
|
+
try {
|
|
580
|
+
execSync(`systemctl is-active portok@${name}`, { stdio: 'pipe' });
|
|
581
|
+
// Service is running, stop it
|
|
582
|
+
try {
|
|
583
|
+
execSync(`systemctl stop portok@${name}`, { stdio: 'pipe' });
|
|
584
|
+
results.push({ action: 'stop', status: 'success' });
|
|
585
|
+
} catch (e) {
|
|
586
|
+
results.push({ action: 'stop', status: 'error', error: e.message });
|
|
587
|
+
}
|
|
588
|
+
} catch {
|
|
589
|
+
// Service not running, skip
|
|
590
|
+
results.push({ action: 'stop', status: 'skipped' });
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// 2. Disable the service
|
|
594
|
+
try {
|
|
595
|
+
execSync(`systemctl is-enabled portok@${name}`, { stdio: 'pipe' });
|
|
596
|
+
// Service is enabled, disable it
|
|
597
|
+
try {
|
|
598
|
+
execSync(`systemctl disable portok@${name}`, { stdio: 'pipe' });
|
|
599
|
+
results.push({ action: 'disable', status: 'success' });
|
|
600
|
+
} catch (e) {
|
|
601
|
+
results.push({ action: 'disable', status: 'error', error: e.message });
|
|
602
|
+
}
|
|
603
|
+
} catch {
|
|
604
|
+
// Service not enabled, skip
|
|
605
|
+
results.push({ action: 'disable', status: 'skipped' });
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// 3. Remove config file
|
|
609
|
+
try {
|
|
610
|
+
fs.unlinkSync(envFilePath);
|
|
611
|
+
results.push({ action: 'remove-config', status: 'success', path: envFilePath });
|
|
612
|
+
} catch (err) {
|
|
613
|
+
results.push({ action: 'remove-config', status: 'error', error: err.message });
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// 4. Remove state file (unless --keep-state)
|
|
617
|
+
if (!options['keep-state']) {
|
|
618
|
+
if (fs.existsSync(stateFilePath)) {
|
|
619
|
+
try {
|
|
620
|
+
fs.unlinkSync(stateFilePath);
|
|
621
|
+
results.push({ action: 'remove-state', status: 'success', path: stateFilePath });
|
|
622
|
+
} catch (err) {
|
|
623
|
+
results.push({ action: 'remove-state', status: 'error', error: err.message });
|
|
624
|
+
}
|
|
625
|
+
} else {
|
|
626
|
+
results.push({ action: 'remove-state', status: 'skipped' });
|
|
627
|
+
}
|
|
628
|
+
} else {
|
|
629
|
+
results.push({ action: 'remove-state', status: 'kept' });
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const hasError = results.some(r => r.status === 'error');
|
|
633
|
+
|
|
634
|
+
if (options.json) {
|
|
635
|
+
console.log(JSON.stringify({ success: !hasError, name, results }, null, 2));
|
|
636
|
+
return hasError ? 1 : 0;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Display results
|
|
640
|
+
console.log(`\n${colors.bold}Removing service '${name}'...${colors.reset}\n`);
|
|
641
|
+
|
|
642
|
+
for (const r of results) {
|
|
643
|
+
const icon = r.status === 'success' ? `${colors.green}✓${colors.reset}` :
|
|
644
|
+
r.status === 'skipped' ? `${colors.dim}○${colors.reset}` :
|
|
645
|
+
r.status === 'kept' ? `${colors.dim}○${colors.reset}` :
|
|
646
|
+
`${colors.red}✗${colors.reset}`;
|
|
647
|
+
|
|
648
|
+
const actionLabel = {
|
|
649
|
+
'stop': 'Stop service',
|
|
650
|
+
'disable': 'Disable service',
|
|
651
|
+
'remove-config': 'Remove config',
|
|
652
|
+
'remove-state': 'Remove state',
|
|
653
|
+
}[r.action];
|
|
654
|
+
|
|
655
|
+
const statusLabel = r.status === 'success' ? 'Done' :
|
|
656
|
+
r.status === 'skipped' ? 'Skipped (not running/enabled)' :
|
|
657
|
+
r.status === 'kept' ? 'Kept (--keep-state)' :
|
|
658
|
+
`Error: ${r.error}`;
|
|
659
|
+
|
|
660
|
+
console.log(` ${icon} ${actionLabel.padEnd(16)} ${statusLabel}`);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (hasError) {
|
|
664
|
+
console.log(`\n${colors.yellow}Warning:${colors.reset} Some operations failed. Check errors above.`);
|
|
665
|
+
return 1;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
console.log(`\n${colors.green}✓ Service '${name}' removed successfully!${colors.reset}`);
|
|
669
|
+
return 0;
|
|
670
|
+
}
|
|
671
|
+
|
|
498
672
|
async function cmdList(options) {
|
|
499
673
|
const configDir = ENV_FILE_DIR;
|
|
500
674
|
|
|
@@ -728,6 +902,7 @@ ${colors.bold}COMMANDS${colors.reset}
|
|
|
728
902
|
${colors.cyan}Management:${colors.reset}
|
|
729
903
|
init Initialize portok directories (/etc/portok, /var/lib/portok)
|
|
730
904
|
add <name> Create a new service instance
|
|
905
|
+
remove <name> Remove a service instance (config + state)
|
|
731
906
|
list List all configured instances and their status
|
|
732
907
|
|
|
733
908
|
${colors.cyan}Service Control:${colors.reset}
|
|
@@ -757,6 +932,10 @@ ${colors.bold}OPTIONS${colors.reset}
|
|
|
757
932
|
--health <path> Health check path (default: /health)
|
|
758
933
|
--force Overwrite existing config
|
|
759
934
|
|
|
935
|
+
${colors.dim}For 'remove' command:${colors.reset}
|
|
936
|
+
--force Skip confirmation prompt
|
|
937
|
+
--keep-state Keep state file (/var/lib/portok/<name>.json)
|
|
938
|
+
|
|
760
939
|
${colors.dim}For 'logs' command:${colors.reset}
|
|
761
940
|
--follow, -f Follow log output
|
|
762
941
|
--lines, -n Number of lines to show (default: 50)
|
|
@@ -779,6 +958,9 @@ ${colors.bold}EXAMPLES${colors.reset}
|
|
|
779
958
|
sudo portok restart api
|
|
780
959
|
portok logs api --follow
|
|
781
960
|
|
|
961
|
+
${colors.dim}# Remove a service${colors.reset}
|
|
962
|
+
sudo portok remove api --force
|
|
963
|
+
|
|
782
964
|
${colors.dim}# Check status of an instance${colors.reset}
|
|
783
965
|
portok status --instance api
|
|
784
966
|
|
|
@@ -813,7 +995,7 @@ async function main() {
|
|
|
813
995
|
}
|
|
814
996
|
|
|
815
997
|
// Management commands (don't require daemon connection)
|
|
816
|
-
const managementCommands = ['init', 'add', 'list', 'start', 'stop', 'restart', 'enable', 'disable', 'logs'];
|
|
998
|
+
const managementCommands = ['init', 'add', 'remove', 'list', 'start', 'stop', 'restart', 'enable', 'disable', 'logs'];
|
|
817
999
|
|
|
818
1000
|
if (managementCommands.includes(args.command)) {
|
|
819
1001
|
let exitCode = 1;
|
|
@@ -824,6 +1006,9 @@ async function main() {
|
|
|
824
1006
|
case 'add':
|
|
825
1007
|
exitCode = await cmdAdd(args.positional[0], args.options);
|
|
826
1008
|
break;
|
|
1009
|
+
case 'remove':
|
|
1010
|
+
exitCode = await cmdRemove(args.positional[0], args.options);
|
|
1011
|
+
break;
|
|
827
1012
|
case 'list':
|
|
828
1013
|
exitCode = await cmdList(args.options);
|
|
829
1014
|
break;
|
package/portok@.service
CHANGED
package/test/drain.test.js
CHANGED
|
@@ -210,25 +210,34 @@ describe('Connection Draining', () => {
|
|
|
210
210
|
|
|
211
211
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
212
212
|
|
|
213
|
-
// Start continuous traffic
|
|
213
|
+
// Start continuous traffic using a more reliable approach
|
|
214
214
|
let requestCount = 0;
|
|
215
215
|
let errorCount = 0;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
216
|
+
let running = true;
|
|
217
|
+
|
|
218
|
+
// Use a loop instead of setInterval for more reliable timing
|
|
219
|
+
const trafficLoop = async () => {
|
|
220
|
+
while (running) {
|
|
221
|
+
try {
|
|
222
|
+
const res = await fetch(`http://127.0.0.1:${proxyPort}/`);
|
|
223
|
+
if (res.ok) {
|
|
224
|
+
requestCount++;
|
|
225
|
+
} else {
|
|
226
|
+
errorCount++;
|
|
227
|
+
}
|
|
228
|
+
} catch {
|
|
223
229
|
errorCount++;
|
|
224
230
|
}
|
|
225
|
-
|
|
226
|
-
|
|
231
|
+
// Small delay between requests
|
|
232
|
+
await new Promise(resolve => setTimeout(resolve, 30));
|
|
227
233
|
}
|
|
228
|
-
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// Start traffic in background
|
|
237
|
+
const trafficPromise = trafficLoop();
|
|
229
238
|
|
|
230
239
|
// Wait a bit, then switch
|
|
231
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
240
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
232
241
|
|
|
233
242
|
await fetch(`http://127.0.0.1:${proxyPort}/__switch?port=${mockServer2.port}`, {
|
|
234
243
|
method: 'POST',
|
|
@@ -236,12 +245,15 @@ describe('Connection Draining', () => {
|
|
|
236
245
|
});
|
|
237
246
|
|
|
238
247
|
// Continue traffic during switch
|
|
239
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
248
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
240
249
|
|
|
241
|
-
|
|
250
|
+
// Stop traffic
|
|
251
|
+
running = false;
|
|
252
|
+
await trafficPromise;
|
|
242
253
|
|
|
243
254
|
// Should have processed requests with minimal errors
|
|
244
|
-
|
|
255
|
+
// At least 3 requests is enough to verify no drops during switch
|
|
256
|
+
assert.ok(requestCount >= 3, `Expected at least 3 requests, got ${requestCount}`);
|
|
245
257
|
assert.strictEqual(errorCount, 0, `Expected 0 errors, got ${errorCount}`);
|
|
246
258
|
});
|
|
247
259
|
});
|