farming-code 2.2.19 → 2.2.22
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 +5 -5
- package/README.zh_cn.md +5 -5
- package/THIRD_PARTY_NOTICES.md +5 -8
- package/backend/acp/client-services.js +37 -5
- package/backend/acp/patch-decisions.js +19 -3
- package/backend/acp-runtime.js +1201 -198
- package/backend/acp-transcript.js +66 -0
- package/backend/agent-lifecycle-journal.js +133 -0
- package/backend/agent-manager.js +2715 -354
- package/backend/agent-order.js +8 -4
- package/backend/agent-runtime-binding.js +1 -0
- package/backend/bundle-update-helper.js +5 -1
- package/backend/config-manager.js +16 -0
- package/backend/control-api.js +146 -20
- package/backend/farming-app-cli.js +335 -10
- package/backend/farming-session-store.js +283 -36
- package/backend/json-cli-runtime.js +120 -6
- package/backend/npm-update-helper.js +97 -18
- package/backend/provider-adapters.js +18 -6
- package/backend/review-session-service.js +31 -17
- package/backend/server.js +337 -103
- package/backend/storage-layout.js +10 -0
- package/backend/terminal-screen-worker-pool.js +15 -6
- package/backend/theme-manager.js +23 -5
- package/backend/update-service.js +239 -27
- package/backend/usage-monitor.js +11 -0
- package/backend/workspace-file-router.js +9 -3
- package/backend/workspace-file-service.js +292 -62
- package/dist/acp/codex-acp-1.1.4.js +32 -1
- package/dist/assets/App-CmZdNavW.js +210 -0
- package/dist/assets/{FileEditorMarkdownPreview-R0jwwLME.js → FileEditorMarkdownPreview-KbnE8VC8.js} +1 -1
- package/dist/assets/FileEditorPane-5_HUejjd.js +2 -0
- package/dist/assets/IconGlyphs-C2WFcoJS.js +1 -0
- package/dist/assets/ProjectFilesSection-C1RAJWOE.js +12 -0
- package/dist/assets/{ReviewPage-DBKNOWsJ.js → ReviewPage-L1vh1Ggs.js} +3 -3
- package/dist/assets/code-dark-BKtvf0Xc.css +1 -0
- package/dist/assets/file-icons-CHWt5nSy.js +1 -0
- package/dist/assets/{index-DQdtPfCk.js → index-RvUiMpGP.js} +2 -2
- package/dist/assets/main-XMXgyJJt.css +1 -0
- package/dist/assets/workspace-view-state-BqB4HM7r.js +7 -0
- package/dist/index.html +1 -1
- package/frontend/skins/crt/app.js +365 -107
- package/frontend/skins/crt/index.html +5 -10
- package/frontend/skins/crt/styles/billing.css +6 -1
- package/package.json +9 -3
- package/dist/assets/App-CbXYBccJ.js +0 -210
- package/dist/assets/FileEditorPane-CJs6kfZO.js +0 -2
- package/dist/assets/IconGlyphs-CZzDr5o2.js +0 -1
- package/dist/assets/ProjectFilesSection-DWCV07lA.js +0 -12
- package/dist/assets/code-dark-qIBiLBWV.css +0 -1
- package/dist/assets/file-icons-DpbrD5ZP.js +0 -1
- package/dist/assets/main-yzZEfgft.css +0 -1
- package/dist/assets/workspace-view-state-BpPINu84.js +0 -7
|
@@ -18,6 +18,9 @@ const DEFAULT_PORT = '6694';
|
|
|
18
18
|
const DEFAULT_BASE_PATH = '/farming';
|
|
19
19
|
const DEFAULT_SERVER_START_TIMEOUT_MS = 30_000;
|
|
20
20
|
const DEFAULT_SERVER_START_STABILITY_MS = 1_500;
|
|
21
|
+
const DEFAULT_SERVER_STOP_TIMEOUT_MS = 30_000;
|
|
22
|
+
const SERVER_STOP_POLL_MS = 100;
|
|
23
|
+
const SERVER_PROCESS_IDENTITY_FORMAT = 'ps-lstart-c-utc-v1';
|
|
21
24
|
const SERVER_COMMANDS = new Set(['start', 'serve', 'daemon', 'stop', 'status', 'logs', 'url', 'help']);
|
|
22
25
|
const CONTROL_COMMANDS = new Set(['skills', 'memory', 'report', 'list', 'spawn', 'output', 'send', 'kill']);
|
|
23
26
|
const SERVER_BACKED_CONTROL_COMMANDS = new Set(['list', 'spawn', 'output', 'send', 'kill']);
|
|
@@ -42,14 +45,19 @@ function quoteShellArg(arg) {
|
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
function buildCleanEnvExecCommand(env, command, args = []) {
|
|
45
|
-
const parts = ['/usr/bin/env',
|
|
48
|
+
const parts = ['/usr/bin/env', ...buildCleanEnvExecArgs(env, command, args)];
|
|
49
|
+
return parts.map(quoteShellArg).join(' ');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildCleanEnvExecArgs(env, command, args = []) {
|
|
53
|
+
const parts = ['-i'];
|
|
46
54
|
Object.entries(env || {}).forEach(([key, value]) => {
|
|
47
55
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) return;
|
|
48
56
|
if (value === undefined || value === null) return;
|
|
49
57
|
parts.push(`${key}=${String(value)}`);
|
|
50
58
|
});
|
|
51
59
|
parts.push(command, ...args);
|
|
52
|
-
return parts
|
|
60
|
+
return parts;
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
function readMemoryLimitBytes() {
|
|
@@ -424,7 +432,13 @@ async function runServerBackedControlCli(argv) {
|
|
|
424
432
|
|
|
425
433
|
function childInvocation(env = process.env) {
|
|
426
434
|
if (process.pkg) {
|
|
427
|
-
|
|
435
|
+
// Spawn env directly so it execs the packaged binary in-place. A shell
|
|
436
|
+
// wrapper may keep its own PID while the listener belongs to a child,
|
|
437
|
+
// making daemon ownership impossible to prove on Linux.
|
|
438
|
+
return {
|
|
439
|
+
command: '/usr/bin/env',
|
|
440
|
+
args: buildCleanEnvExecArgs(env, process.execPath, ['--']),
|
|
441
|
+
};
|
|
428
442
|
}
|
|
429
443
|
const nodePath = env.FARMING_NODE_BIN || process.execPath;
|
|
430
444
|
if (env.FARMING_NODE_LD && env.FARMING_NODE_LIBRARY_PATH) {
|
|
@@ -460,17 +474,271 @@ function readServerState(configDir) {
|
|
|
460
474
|
}
|
|
461
475
|
}
|
|
462
476
|
|
|
463
|
-
function
|
|
477
|
+
function canonicalConfigDir(configDir) {
|
|
478
|
+
try {
|
|
479
|
+
return fs.realpathSync.native(configDir);
|
|
480
|
+
} catch {
|
|
481
|
+
return path.resolve(configDir);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async function readServerProcessIdentity(pid) {
|
|
486
|
+
const processId = Number(pid);
|
|
487
|
+
if (!Number.isSafeInteger(processId) || processId <= 0 || process.platform === 'win32') return null;
|
|
488
|
+
let stdout;
|
|
489
|
+
try {
|
|
490
|
+
stdout = execFileSync(
|
|
491
|
+
'/bin/ps',
|
|
492
|
+
['-p', String(processId), '-o', 'pid=', '-o', 'pgid=', '-o', 'lstart='],
|
|
493
|
+
{
|
|
494
|
+
encoding: 'utf8',
|
|
495
|
+
timeout: 1_000,
|
|
496
|
+
maxBuffer: 16_384,
|
|
497
|
+
env: { ...process.env, LANG: 'C', LC_ALL: 'C', TZ: 'UTC' },
|
|
498
|
+
},
|
|
499
|
+
);
|
|
500
|
+
} catch (error) {
|
|
501
|
+
if (error?.status === 1 || error?.code === 'ESRCH') return null;
|
|
502
|
+
throw error;
|
|
503
|
+
}
|
|
504
|
+
const match = String(stdout || '').trim().match(/^(\d+)\s+(\d+)\s+(.+)$/);
|
|
505
|
+
if (!match || Number(match[1]) !== processId) return null;
|
|
506
|
+
return {
|
|
507
|
+
pid: processId,
|
|
508
|
+
processGroupId: Number(match[2]),
|
|
509
|
+
startedAt: match[3].trim(),
|
|
510
|
+
format: SERVER_PROCESS_IDENTITY_FORMAT,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function writeServerState(configDir, env, pid, processIdentity) {
|
|
464
515
|
const state = {
|
|
465
516
|
pid,
|
|
466
517
|
port: Number(env.PORT || DEFAULT_PORT),
|
|
467
518
|
basePath: env.FARMING_BASE_PATH || DEFAULT_BASE_PATH,
|
|
468
|
-
configDir,
|
|
519
|
+
configDir: canonicalConfigDir(configDir),
|
|
520
|
+
processIdentity,
|
|
469
521
|
updatedAt: new Date().toISOString(),
|
|
470
522
|
};
|
|
471
523
|
fs.writeFileSync(serverStateFile(configDir), `${JSON.stringify(state, null, 2)}\n`);
|
|
472
524
|
}
|
|
473
525
|
|
|
526
|
+
function processLooksLikeFarmingServer(pid) {
|
|
527
|
+
let environment = '';
|
|
528
|
+
try {
|
|
529
|
+
environment = fs.readFileSync(`/proc/${pid}/environ`, 'utf8').replace(/\0/g, ' ');
|
|
530
|
+
} catch {
|
|
531
|
+
try {
|
|
532
|
+
environment = execFileSync(
|
|
533
|
+
'/bin/ps',
|
|
534
|
+
['eww', '-p', String(pid), '-o', 'command='],
|
|
535
|
+
{ encoding: 'utf8', timeout: 1_000, maxBuffer: 1024 * 1024 },
|
|
536
|
+
);
|
|
537
|
+
} catch {
|
|
538
|
+
environment = '';
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (/(?:^|\s)FARMING_RUN_SERVER=1(?:\s|$)/.test(environment)) return true;
|
|
542
|
+
return /(?:^|\s)(?:\S+[/\\])?backend[/\\]server\.js(?:\s|$)/.test(environment);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function processHasExactEnvironment(pid, expected) {
|
|
546
|
+
try {
|
|
547
|
+
const entries = fs.readFileSync(`/proc/${pid}/environ`, 'utf8')
|
|
548
|
+
.split('\0')
|
|
549
|
+
.filter(Boolean)
|
|
550
|
+
.map(entry => {
|
|
551
|
+
const separator = entry.indexOf('=');
|
|
552
|
+
return separator < 0 ? [entry, ''] : [entry.slice(0, separator), entry.slice(separator + 1)];
|
|
553
|
+
});
|
|
554
|
+
const environment = new Map(entries);
|
|
555
|
+
return Object.entries(expected).every(([key, value]) => environment.get(key) === String(value));
|
|
556
|
+
} catch {
|
|
557
|
+
if (Object.values(expected).some(value => /\s/.test(String(value)))) return false;
|
|
558
|
+
try {
|
|
559
|
+
const environment = execFileSync(
|
|
560
|
+
'/bin/ps',
|
|
561
|
+
['eww', '-p', String(pid), '-o', 'command='],
|
|
562
|
+
{ encoding: 'utf8', timeout: 1_000, maxBuffer: 1024 * 1024 },
|
|
563
|
+
);
|
|
564
|
+
return Object.entries(expected).every(([key, value]) => {
|
|
565
|
+
const escaped = `${key}=${value}`.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
566
|
+
return new RegExp(`(?:^|\\s)${escaped}(?:\\s|$)`).test(environment);
|
|
567
|
+
});
|
|
568
|
+
} catch {
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function readLegacyServerSettings(configDir, state, pid) {
|
|
575
|
+
const authDisabled = processHasExactEnvironment(pid, { FARMING_DISABLE_AUTH: '1' });
|
|
576
|
+
const token = authDisabled ? '' : readTokenForEnv({ FARMING_CONFIG_DIR: configDir });
|
|
577
|
+
if (!authDisabled && !token) {
|
|
578
|
+
return Promise.reject(new Error('legacy Farming server identity probe requires this config directory token'));
|
|
579
|
+
}
|
|
580
|
+
const requestPath = routePath(state.basePath || DEFAULT_BASE_PATH, '/api/settings');
|
|
581
|
+
return new Promise((resolve, reject) => {
|
|
582
|
+
const req = http.request({
|
|
583
|
+
host: '127.0.0.1',
|
|
584
|
+
port: Number(state.port),
|
|
585
|
+
path: requestPath,
|
|
586
|
+
method: 'GET',
|
|
587
|
+
timeout: 1_000,
|
|
588
|
+
headers: token ? { Cookie: `farming_token=${encodeURIComponent(token)}` } : {},
|
|
589
|
+
}, res => {
|
|
590
|
+
let body = '';
|
|
591
|
+
res.setEncoding('utf8');
|
|
592
|
+
res.on('data', chunk => {
|
|
593
|
+
if (body.length <= 64 * 1024) body += chunk;
|
|
594
|
+
});
|
|
595
|
+
res.on('end', () => {
|
|
596
|
+
if (res.statusCode !== 200) {
|
|
597
|
+
reject(new Error(`legacy Farming server identity probe returned HTTP ${res.statusCode || 0}`));
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
try {
|
|
601
|
+
const payload = JSON.parse(body);
|
|
602
|
+
if (!payload?.settings || typeof payload.settings !== 'object') {
|
|
603
|
+
reject(new Error('legacy Farming server identity probe did not return Farming settings'));
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
resolve(payload.settings);
|
|
607
|
+
} catch (error) {
|
|
608
|
+
reject(new Error(`legacy Farming server identity probe was invalid: ${error.message || error}`));
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
req.on('error', reject);
|
|
613
|
+
req.on('timeout', () => req.destroy(new Error('legacy Farming server identity probe timed out')));
|
|
614
|
+
req.end();
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function processOwnsListeningPort(pid, port) {
|
|
619
|
+
const processId = Number(pid);
|
|
620
|
+
const portNumber = Number(port);
|
|
621
|
+
if (!Number.isSafeInteger(processId) || processId <= 0 || !Number.isInteger(portNumber) || portNumber <= 0) {
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
if (process.platform === 'linux') {
|
|
625
|
+
try {
|
|
626
|
+
const socketInodes = new Set(fs.readdirSync(`/proc/${processId}/fd`).flatMap(entry => {
|
|
627
|
+
try {
|
|
628
|
+
const target = fs.readlinkSync(`/proc/${processId}/fd/${entry}`);
|
|
629
|
+
const match = target.match(/^socket:\[(\d+)\]$/);
|
|
630
|
+
return match ? [match[1]] : [];
|
|
631
|
+
} catch {
|
|
632
|
+
return [];
|
|
633
|
+
}
|
|
634
|
+
}));
|
|
635
|
+
const portHex = portNumber.toString(16).toUpperCase().padStart(4, '0');
|
|
636
|
+
for (const table of ['/proc/net/tcp', '/proc/net/tcp6']) {
|
|
637
|
+
let rows = [];
|
|
638
|
+
try {
|
|
639
|
+
rows = fs.readFileSync(table, 'utf8').trim().split(/\r?\n/).slice(1);
|
|
640
|
+
} catch {
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
if (rows.some(row => {
|
|
644
|
+
const fields = row.trim().split(/\s+/);
|
|
645
|
+
const localPort = String(fields[1] || '').split(':').at(-1);
|
|
646
|
+
return localPort === portHex && fields[3] === '0A' && socketInodes.has(fields[9]);
|
|
647
|
+
})) return true;
|
|
648
|
+
}
|
|
649
|
+
return false;
|
|
650
|
+
} catch {
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
try {
|
|
655
|
+
const lsof = fs.existsSync('/usr/sbin/lsof') ? '/usr/sbin/lsof' : 'lsof';
|
|
656
|
+
const stdout = execFileSync(
|
|
657
|
+
lsof,
|
|
658
|
+
['-nP', '-a', '-p', String(processId), `-iTCP:${portNumber}`, '-sTCP:LISTEN', '-Fpfn'],
|
|
659
|
+
{ encoding: 'utf8', timeout: 1_000, maxBuffer: 64 * 1024 },
|
|
660
|
+
);
|
|
661
|
+
return String(stdout || '').split(/\r?\n/).some(line => line.startsWith('n') && line.endsWith(`:${portNumber}`));
|
|
662
|
+
} catch {
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function matchingProcessIdentity(expected, current) {
|
|
668
|
+
return Boolean(
|
|
669
|
+
current
|
|
670
|
+
&& current.pid === Number(expected?.pid)
|
|
671
|
+
&& current.processGroupId === Number(expected?.processGroupId)
|
|
672
|
+
&& current.startedAt === String(expected?.startedAt || '')
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
async function assertServerProcessIdentity(configDir, pid, state) {
|
|
677
|
+
const expected = state?.processIdentity;
|
|
678
|
+
if (
|
|
679
|
+
Number(state?.pid) !== pid
|
|
680
|
+
|| canonicalConfigDir(state?.configDir || '') !== canonicalConfigDir(configDir)
|
|
681
|
+
|| !expected
|
|
682
|
+
|| Number(expected.pid) !== pid
|
|
683
|
+
|| !Number.isSafeInteger(Number(expected.processGroupId))
|
|
684
|
+
|| Number(expected.processGroupId) <= 0
|
|
685
|
+
|| !String(expected.startedAt || '').trim()
|
|
686
|
+
) {
|
|
687
|
+
throw new Error(
|
|
688
|
+
`Refusing to stop Farming PID ${pid}: server control metadata does not contain a matching process identity`,
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
const current = await readServerProcessIdentity(pid);
|
|
692
|
+
if (!matchingProcessIdentity(expected, current)) {
|
|
693
|
+
throw new Error(
|
|
694
|
+
`Refusing to stop Farming PID ${pid}: the live process identity does not match the server control metadata`,
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
if (!processOwnsListeningPort(pid, Number(state.port))) {
|
|
698
|
+
throw new Error(`Refusing to stop Farming PID ${pid}: the process does not own listening port ${state.port}`);
|
|
699
|
+
}
|
|
700
|
+
return current;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async function migrateLegacyServerIdentity(configDir, pid, state) {
|
|
704
|
+
if (
|
|
705
|
+
Number(state?.pid) !== pid
|
|
706
|
+
|| canonicalConfigDir(state?.configDir || '') !== canonicalConfigDir(configDir)
|
|
707
|
+
|| !Number.isInteger(Number(state?.port))
|
|
708
|
+
|| Number(state.port) <= 0
|
|
709
|
+
|| Number(state.port) > 65535
|
|
710
|
+
|| !processLooksLikeFarmingServer(pid)
|
|
711
|
+
|| !processHasExactEnvironment(pid, {
|
|
712
|
+
FARMING_CONFIG_DIR: configDir,
|
|
713
|
+
PORT: String(state.port),
|
|
714
|
+
})
|
|
715
|
+
) {
|
|
716
|
+
throw new Error(
|
|
717
|
+
`Refusing to stop Farming PID ${pid}: legacy server control metadata could not prove this process belongs to the config directory`,
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
const firstIdentity = await readServerProcessIdentity(pid);
|
|
721
|
+
if (!firstIdentity) {
|
|
722
|
+
throw new Error(`Refusing to stop Farming PID ${pid}: the legacy server process identity could not be read`);
|
|
723
|
+
}
|
|
724
|
+
if (!processOwnsListeningPort(pid, Number(state.port))) {
|
|
725
|
+
throw new Error(`Refusing to stop Farming PID ${pid}: the legacy process does not own listening port ${state.port}`);
|
|
726
|
+
}
|
|
727
|
+
await readLegacyServerSettings(configDir, state, pid);
|
|
728
|
+
const stableIdentity = await readServerProcessIdentity(pid);
|
|
729
|
+
if (!matchingProcessIdentity(firstIdentity, stableIdentity) || readPid(configDir) !== pid) {
|
|
730
|
+
throw new Error(`Refusing to stop Farming PID ${pid}: the legacy server process identity changed during verification`);
|
|
731
|
+
}
|
|
732
|
+
fs.writeFileSync(serverStateFile(configDir), `${JSON.stringify({
|
|
733
|
+
...state,
|
|
734
|
+
pid,
|
|
735
|
+
configDir: canonicalConfigDir(configDir),
|
|
736
|
+
processIdentity: stableIdentity,
|
|
737
|
+
identityMigratedAt: new Date().toISOString(),
|
|
738
|
+
}, null, 2)}\n`);
|
|
739
|
+
return stableIdentity;
|
|
740
|
+
}
|
|
741
|
+
|
|
474
742
|
function readPid(configDir) {
|
|
475
743
|
try {
|
|
476
744
|
return Number(fs.readFileSync(pidFile(configDir), 'utf8').trim());
|
|
@@ -523,6 +791,36 @@ function serverStartStabilityMs(env) {
|
|
|
523
791
|
return DEFAULT_SERVER_START_STABILITY_MS;
|
|
524
792
|
}
|
|
525
793
|
|
|
794
|
+
function serverStopTimeoutMs(env) {
|
|
795
|
+
const parsed = Number(env.FARMING_STOP_TIMEOUT_MS || env.FARMING_SERVER_STOP_TIMEOUT_MS);
|
|
796
|
+
if (Number.isFinite(parsed) && parsed > 0) return Math.min(parsed, 120_000);
|
|
797
|
+
return DEFAULT_SERVER_STOP_TIMEOUT_MS;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
async function waitForDaemonStop(pid, port, options = {}) {
|
|
801
|
+
const timeoutMs = options.timeoutMs || DEFAULT_SERVER_STOP_TIMEOUT_MS;
|
|
802
|
+
const processRunning = options.isRunning || isRunning;
|
|
803
|
+
const portAvailable = options.canListenOnPort || canListenOnPort;
|
|
804
|
+
const now = options.now || Date.now;
|
|
805
|
+
const wait = options.wait || (durationMs => new Promise(resolve => setTimeout(resolve, durationMs)));
|
|
806
|
+
const startedAt = now();
|
|
807
|
+
let running = true;
|
|
808
|
+
let released = false;
|
|
809
|
+
|
|
810
|
+
while (true) {
|
|
811
|
+
running = processRunning(pid);
|
|
812
|
+
released = await portAvailable(port);
|
|
813
|
+
if (!running && released) return;
|
|
814
|
+
if (now() - startedAt >= timeoutMs) {
|
|
815
|
+
throw new Error(
|
|
816
|
+
`Farming server PID ${pid} did not stop and release port ${port} within ${timeoutMs}ms`
|
|
817
|
+
+ ` (process ${running ? 'still running' : 'exited'}, port ${released ? 'released' : 'still in use'})`,
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
await wait(SERVER_STOP_POLL_MS);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
526
824
|
function waitForProcessStability(pid, durationMs = DEFAULT_SERVER_START_STABILITY_MS) {
|
|
527
825
|
const startedAt = Date.now();
|
|
528
826
|
return new Promise((resolve, reject) => {
|
|
@@ -682,9 +980,20 @@ async function startDaemon(parsed) {
|
|
|
682
980
|
fs.writeFileSync(pidFile(configDir), String(child.pid));
|
|
683
981
|
|
|
684
982
|
try {
|
|
983
|
+
const processIdentity = await readServerProcessIdentity(child.pid);
|
|
984
|
+
if (!processIdentity) throw new Error('server process identity could not be verified after launch');
|
|
685
985
|
await waitForServer(env, serverStartTimeoutMs(env), child.pid);
|
|
686
986
|
await waitForProcessStability(child.pid, serverStartStabilityMs(env));
|
|
687
987
|
await waitForServer(env, Math.min(serverStartTimeoutMs(env), 5_000), child.pid);
|
|
988
|
+
const stableIdentity = await readServerProcessIdentity(child.pid);
|
|
989
|
+
if (
|
|
990
|
+
!stableIdentity
|
|
991
|
+
|| stableIdentity.processGroupId !== processIdentity.processGroupId
|
|
992
|
+
|| stableIdentity.startedAt !== processIdentity.startedAt
|
|
993
|
+
) {
|
|
994
|
+
throw new Error('server process identity changed during startup');
|
|
995
|
+
}
|
|
996
|
+
writeServerState(configDir, env, child.pid, stableIdentity);
|
|
688
997
|
} catch (error) {
|
|
689
998
|
cleanupFailedDaemonStart(configDir, child.pid);
|
|
690
999
|
console.error(error.message);
|
|
@@ -694,7 +1003,6 @@ async function startDaemon(parsed) {
|
|
|
694
1003
|
}
|
|
695
1004
|
|
|
696
1005
|
console.log(`Farming started (PID ${child.pid})`);
|
|
697
|
-
writeServerState(configDir, env, child.pid);
|
|
698
1006
|
const logs = tailFile(logFile(configDir), 40);
|
|
699
1007
|
const urlLines = logs.split(/\r?\n/).filter(line => /Local:|Network:|Token:|Token style:|Token auth:/.test(line));
|
|
700
1008
|
if (urlLines.length > 0) {
|
|
@@ -705,7 +1013,7 @@ async function startDaemon(parsed) {
|
|
|
705
1013
|
return 0;
|
|
706
1014
|
}
|
|
707
1015
|
|
|
708
|
-
function stopDaemon(parsed) {
|
|
1016
|
+
async function stopDaemon(parsed) {
|
|
709
1017
|
const env = buildServerEnv(parsed.env);
|
|
710
1018
|
const configDir = env.FARMING_CONFIG_DIR;
|
|
711
1019
|
const pid = readPid(configDir);
|
|
@@ -715,9 +1023,21 @@ function stopDaemon(parsed) {
|
|
|
715
1023
|
console.log('Farming is not running.');
|
|
716
1024
|
return 0;
|
|
717
1025
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
1026
|
+
const state = readServerState(configDir);
|
|
1027
|
+
const port = Number(state.port || env.PORT || DEFAULT_PORT);
|
|
1028
|
+
if (state.processIdentity?.format === SERVER_PROCESS_IDENTITY_FORMAT) {
|
|
1029
|
+
await assertServerProcessIdentity(configDir, pid, state);
|
|
1030
|
+
} else {
|
|
1031
|
+
await migrateLegacyServerIdentity(configDir, pid, state);
|
|
1032
|
+
}
|
|
1033
|
+
try {
|
|
1034
|
+
process.kill(pid, 'SIGTERM');
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
if (error?.code !== 'ESRCH') throw error;
|
|
1037
|
+
}
|
|
1038
|
+
await waitForDaemonStop(pid, port, { timeoutMs: serverStopTimeoutMs(env) });
|
|
1039
|
+
if (readPid(configDir) === pid) fs.rmSync(pidFile(configDir), { force: true });
|
|
1040
|
+
if (Number(readServerState(configDir).pid) === pid) fs.rmSync(serverStateFile(configDir), { force: true });
|
|
721
1041
|
console.log(`Stopped Farming (PID ${pid})`);
|
|
722
1042
|
return 0;
|
|
723
1043
|
}
|
|
@@ -841,6 +1161,7 @@ if (require.main === module) {
|
|
|
841
1161
|
module.exports = {
|
|
842
1162
|
SERVER_MODE_ARG,
|
|
843
1163
|
NATIVE_PTY_HOST_ARG,
|
|
1164
|
+
buildCleanEnvExecArgs,
|
|
844
1165
|
buildCleanEnvExecCommand,
|
|
845
1166
|
childInvocation,
|
|
846
1167
|
cleanupFailedDaemonStart,
|
|
@@ -854,9 +1175,13 @@ module.exports = {
|
|
|
854
1175
|
resolveReviewTarget,
|
|
855
1176
|
reviewUrl,
|
|
856
1177
|
readServerState,
|
|
1178
|
+
readServerProcessIdentity,
|
|
857
1179
|
serverStartTimeoutMs,
|
|
858
1180
|
serverStartStabilityMs,
|
|
1181
|
+
serverStopTimeoutMs,
|
|
859
1182
|
splitControlArgs,
|
|
1183
|
+
stopDaemon,
|
|
860
1184
|
run,
|
|
861
1185
|
serverStateFile,
|
|
1186
|
+
waitForDaemonStop,
|
|
862
1187
|
};
|