@wipcomputer/wip-ldm-os 0.4.85-alpha.26 → 0.4.85-alpha.27
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/bin/ldm.js +21 -15
- package/package.json +1 -1
- package/scripts/test-ldm-status-concurrency.mjs +67 -37
- package/scripts/test-ldm-status-timeout.mjs +43 -27
package/bin/ldm.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, cpSync, chmodSync, unlinkSync, readlinkSync, renameSync, statSync, lstatSync, symlinkSync } from 'node:fs';
|
|
24
24
|
import { join, basename, resolve, dirname } from 'node:path';
|
|
25
|
-
import {
|
|
25
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
26
26
|
import { fileURLToPath } from 'node:url';
|
|
27
27
|
|
|
28
28
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -3285,21 +3285,27 @@ async function cmdDoctor() {
|
|
|
3285
3285
|
const STATUS_NPM_TIMEOUT_MS = parsePositiveInt(process.env.LDM_STATUS_NPM_TIMEOUT_MS, 5000);
|
|
3286
3286
|
const STATUS_TOTAL_BUDGET_MS = parsePositiveInt(process.env.LDM_STATUS_TOTAL_BUDGET_MS, 60000);
|
|
3287
3287
|
const STATUS_NPM_CONCURRENCY = parsePositiveInt(process.env.LDM_STATUS_NPM_CONCURRENCY, 8);
|
|
3288
|
+
const STATUS_NPM_REGISTRY_URL = process.env.LDM_STATUS_NPM_REGISTRY_URL || 'https://registry.npmjs.org';
|
|
3288
3289
|
|
|
3289
|
-
function npmViewVersionForStatus(pkg, timeoutMs) {
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
rejectPromise(error);
|
|
3298
|
-
return;
|
|
3299
|
-
}
|
|
3300
|
-
resolvePromise(String(stdout || '').trim());
|
|
3290
|
+
async function npmViewVersionForStatus(pkg, timeoutMs) {
|
|
3291
|
+
const controller = new AbortController();
|
|
3292
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
3293
|
+
try {
|
|
3294
|
+
const registry = STATUS_NPM_REGISTRY_URL.replace(/\/+$/, '');
|
|
3295
|
+
const response = await fetch(`${registry}/${encodeURIComponent(pkg)}`, {
|
|
3296
|
+
signal: controller.signal,
|
|
3297
|
+
headers: { accept: 'application/vnd.npm.install-v1+json, application/json' },
|
|
3301
3298
|
});
|
|
3302
|
-
|
|
3299
|
+
if (!response.ok) {
|
|
3300
|
+
const error = new Error(`npm registry returned ${response.status}`);
|
|
3301
|
+
error.statusCode = response.status;
|
|
3302
|
+
throw error;
|
|
3303
|
+
}
|
|
3304
|
+
const metadata = await response.json();
|
|
3305
|
+
return metadata?.['dist-tags']?.latest || '';
|
|
3306
|
+
} finally {
|
|
3307
|
+
clearTimeout(timeout);
|
|
3308
|
+
}
|
|
3303
3309
|
}
|
|
3304
3310
|
|
|
3305
3311
|
function remainingStatusBudgetMs(startedAt) {
|
|
@@ -3313,7 +3319,7 @@ function formatStatusElapsed(ms) {
|
|
|
3313
3319
|
}
|
|
3314
3320
|
|
|
3315
3321
|
function classifyStatusCheckError(error) {
|
|
3316
|
-
if (error?.signal === 'SIGTERM' || error?.code === 'ETIMEDOUT' || String(error?.message || '').includes('ETIMEDOUT')) {
|
|
3322
|
+
if (error?.name === 'AbortError' || error?.signal === 'SIGTERM' || error?.code === 'ETIMEDOUT' || String(error?.message || '').includes('ETIMEDOUT')) {
|
|
3317
3323
|
return 'timeout';
|
|
3318
3324
|
}
|
|
3319
3325
|
return 'unavailable';
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { createServer } from 'node:http';
|
|
3
4
|
import { tmpdir } from 'node:os';
|
|
4
5
|
import { dirname, join } from 'node:path';
|
|
5
|
-
import {
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
6
7
|
import { fileURLToPath } from 'node:url';
|
|
7
8
|
|
|
8
9
|
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
@@ -13,7 +14,28 @@ function assert(condition, message) {
|
|
|
13
14
|
if (!condition) throw new Error(message);
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
function
|
|
17
|
+
function listen(server) {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
server.listen(0, '127.0.0.1', () => resolve(server.address()));
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function createRegistryServer(delayMs) {
|
|
24
|
+
let active = 0;
|
|
25
|
+
let maxActive = 0;
|
|
26
|
+
const server = createServer((_req, res) => {
|
|
27
|
+
active += 1;
|
|
28
|
+
maxActive = Math.max(maxActive, active);
|
|
29
|
+
setTimeout(() => {
|
|
30
|
+
res.setHeader('content-type', 'application/json');
|
|
31
|
+
res.end(JSON.stringify({ 'dist-tags': { latest: '1.0.0' } }));
|
|
32
|
+
active -= 1;
|
|
33
|
+
}, delayMs);
|
|
34
|
+
});
|
|
35
|
+
return { server, getMaxActive: () => maxActive };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function writeFixture(home) {
|
|
17
39
|
const extensions = join(home, '.ldm', 'extensions');
|
|
18
40
|
mkdirSync(extensions, { recursive: true });
|
|
19
41
|
writeFileSync(join(home, '.ldm', 'version.json'), JSON.stringify({
|
|
@@ -30,55 +52,63 @@ function writeFixture(home, fakeBin) {
|
|
|
30
52
|
};
|
|
31
53
|
}
|
|
32
54
|
writeFileSync(join(extensions, 'registry.json'), JSON.stringify(registry, null, 2) + '\n');
|
|
33
|
-
|
|
34
|
-
mkdirSync(fakeBin, { recursive: true });
|
|
35
|
-
const fakeNpm = join(fakeBin, 'npm');
|
|
36
|
-
writeFileSync(fakeNpm, `#!/usr/bin/env bash
|
|
37
|
-
if [ "$1" = "view" ]; then
|
|
38
|
-
sleep "\${FAKE_NPM_SLEEP:-1}"
|
|
39
|
-
echo "1.0.0"
|
|
40
|
-
exit 0
|
|
41
|
-
fi
|
|
42
|
-
echo "unexpected npm command: $*" >&2
|
|
43
|
-
exit 64
|
|
44
|
-
`);
|
|
45
|
-
chmodSync(fakeNpm, 0o755);
|
|
46
55
|
}
|
|
47
56
|
|
|
48
|
-
function runStatus({ concurrency,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
function runStatus({ concurrency, registryUrl, home }) {
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
const child = spawn(process.execPath, [join(root, 'bin', 'ldm.js'), 'status'], {
|
|
60
|
+
cwd: root,
|
|
61
|
+
env: {
|
|
62
|
+
...process.env,
|
|
63
|
+
HOME: home,
|
|
64
|
+
LDM_STATUS_NPM_REGISTRY_URL: registryUrl,
|
|
65
|
+
LDM_STATUS_NPM_CONCURRENCY: String(concurrency),
|
|
66
|
+
LDM_STATUS_NPM_TIMEOUT_MS: '2000',
|
|
67
|
+
LDM_STATUS_TOTAL_BUDGET_MS: '10000',
|
|
68
|
+
},
|
|
69
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
let stdout = '';
|
|
73
|
+
let stderr = '';
|
|
74
|
+
child.stdout.setEncoding('utf8');
|
|
75
|
+
child.stderr.setEncoding('utf8');
|
|
76
|
+
child.stdout.on('data', chunk => { stdout += chunk; });
|
|
77
|
+
child.stderr.on('data', chunk => { stderr += chunk; });
|
|
78
|
+
child.on('close', status => resolve({ status, stdout, stderr }));
|
|
79
|
+
});
|
|
80
|
+
}
|
|
52
81
|
|
|
82
|
+
async function runFixture({ concurrency, delayMs }) {
|
|
83
|
+
const home = join(tempRoot, `home-${concurrency}-${delayMs}`);
|
|
84
|
+
writeFixture(home);
|
|
85
|
+
const registry = createRegistryServer(delayMs);
|
|
86
|
+
const address = await listen(registry.server);
|
|
53
87
|
const startedAt = Date.now();
|
|
54
|
-
const result =
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
env: {
|
|
59
|
-
...process.env,
|
|
60
|
-
HOME: home,
|
|
61
|
-
PATH: `${fakeBin}:${process.env.PATH || ''}`,
|
|
62
|
-
FAKE_NPM_SLEEP: String(sleepSeconds),
|
|
63
|
-
LDM_STATUS_NPM_CONCURRENCY: String(concurrency),
|
|
64
|
-
LDM_STATUS_NPM_TIMEOUT_MS: '2000',
|
|
65
|
-
LDM_STATUS_TOTAL_BUDGET_MS: '10000',
|
|
66
|
-
},
|
|
88
|
+
const result = await runStatus({
|
|
89
|
+
concurrency,
|
|
90
|
+
home,
|
|
91
|
+
registryUrl: `http://${address.address}:${address.port}`,
|
|
67
92
|
});
|
|
68
|
-
|
|
93
|
+
const elapsedMs = Date.now() - startedAt;
|
|
94
|
+
registry.server.closeAllConnections();
|
|
95
|
+
registry.server.close();
|
|
96
|
+
return { result, elapsedMs, maxActive: registry.getMaxActive() };
|
|
69
97
|
}
|
|
70
98
|
|
|
71
99
|
try {
|
|
72
|
-
const concurrent =
|
|
100
|
+
const concurrent = await runFixture({ concurrency: 4, delayMs: 500 });
|
|
73
101
|
assert(concurrent.result.status === 0, `concurrent ldm status exited ${concurrent.result.status}\nstdout:\n${concurrent.result.stdout}\nstderr:\n${concurrent.result.stderr}`);
|
|
74
|
-
assert(concurrent.elapsedMs <
|
|
102
|
+
assert(concurrent.elapsedMs < 3000, `concurrent ldm status should finish well before serial runtime; elapsed ${concurrent.elapsedMs}ms`);
|
|
103
|
+
assert(concurrent.maxActive >= 4, `registry server should see concurrent probes; max active ${concurrent.maxActive}`);
|
|
75
104
|
assert(concurrent.result.stdout.includes(`LDM OS v${sourceVersion}`), `status should print installed LDM OS version\n${concurrent.result.stdout}`);
|
|
76
105
|
assert(concurrent.result.stdout.includes('Extensions: 8'), `status should print extension count\n${concurrent.result.stdout}`);
|
|
77
106
|
assert(concurrent.result.stdout.includes('ext-8: checking npm'), `status should check every staged extension\n${concurrent.result.stdout}`);
|
|
78
107
|
assert(!concurrent.result.stdout.includes('Update checks skipped:'), `concurrent status should not skip checks in this fixture\n${concurrent.result.stdout}`);
|
|
79
108
|
|
|
80
|
-
const serialFallback =
|
|
109
|
+
const serialFallback = await runFixture({ concurrency: 1, delayMs: 10 });
|
|
81
110
|
assert(serialFallback.result.status === 0, `serial fallback ldm status exited ${serialFallback.result.status}\nstdout:\n${serialFallback.result.stdout}\nstderr:\n${serialFallback.result.stderr}`);
|
|
111
|
+
assert(serialFallback.maxActive === 1, `serial fallback should only run one probe at a time; max active ${serialFallback.maxActive}`);
|
|
82
112
|
assert(serialFallback.result.stdout.includes('ext-8: checking npm'), `serial fallback should still check every staged extension\n${serialFallback.result.stdout}`);
|
|
83
113
|
assert(!serialFallback.result.stdout.includes('Update checks skipped:'), `serial fallback should not skip checks in this fixture\n${serialFallback.result.stdout}`);
|
|
84
114
|
} finally {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { createServer } from 'node:http';
|
|
3
4
|
import { tmpdir } from 'node:os';
|
|
4
5
|
import { dirname, join } from 'node:path';
|
|
5
|
-
import {
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
6
7
|
import { fileURLToPath } from 'node:url';
|
|
7
8
|
|
|
8
9
|
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
@@ -13,9 +14,38 @@ function assert(condition, message) {
|
|
|
13
14
|
if (!condition) throw new Error(message);
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
function runStatus({ home, registryUrl }) {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
const child = spawn(process.execPath, [join(root, 'bin', 'ldm.js'), 'status'], {
|
|
20
|
+
cwd: root,
|
|
21
|
+
env: {
|
|
22
|
+
...process.env,
|
|
23
|
+
HOME: home,
|
|
24
|
+
LDM_STATUS_NPM_REGISTRY_URL: registryUrl,
|
|
25
|
+
LDM_STATUS_NPM_TIMEOUT_MS: '75',
|
|
26
|
+
LDM_STATUS_TOTAL_BUDGET_MS: '250',
|
|
27
|
+
},
|
|
28
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
let stdout = '';
|
|
32
|
+
let stderr = '';
|
|
33
|
+
child.stdout.setEncoding('utf8');
|
|
34
|
+
child.stderr.setEncoding('utf8');
|
|
35
|
+
child.stdout.on('data', chunk => { stdout += chunk; });
|
|
36
|
+
child.stderr.on('data', chunk => { stderr += chunk; });
|
|
37
|
+
child.on('close', status => resolve({ status, stdout, stderr }));
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function listen(server) {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
server.listen(0, '127.0.0.1', () => resolve(server.address()));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
16
47
|
try {
|
|
17
48
|
const home = join(tempRoot, 'home');
|
|
18
|
-
const fakeBin = join(tempRoot, 'bin');
|
|
19
49
|
const extensions = join(home, '.ldm', 'extensions');
|
|
20
50
|
|
|
21
51
|
mkdirSync(extensions, { recursive: true });
|
|
@@ -37,33 +67,19 @@ try {
|
|
|
37
67
|
},
|
|
38
68
|
}, null, 2) + '\n');
|
|
39
69
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
fi
|
|
48
|
-
echo "unexpected npm command: $*" >&2
|
|
49
|
-
exit 64
|
|
50
|
-
`);
|
|
51
|
-
chmodSync(fakeNpm, 0o755);
|
|
70
|
+
const server = createServer((_req, res) => {
|
|
71
|
+
setTimeout(() => {
|
|
72
|
+
res.setHeader('content-type', 'application/json');
|
|
73
|
+
res.end(JSON.stringify({ 'dist-tags': { latest: '9.9.9' } }));
|
|
74
|
+
}, 2000);
|
|
75
|
+
});
|
|
76
|
+
const address = await listen(server);
|
|
52
77
|
|
|
53
78
|
const startedAt = Date.now();
|
|
54
|
-
const result =
|
|
55
|
-
cwd: root,
|
|
56
|
-
encoding: 'utf8',
|
|
57
|
-
timeout: 3000,
|
|
58
|
-
env: {
|
|
59
|
-
...process.env,
|
|
60
|
-
HOME: home,
|
|
61
|
-
PATH: `${fakeBin}:${process.env.PATH || ''}`,
|
|
62
|
-
LDM_STATUS_NPM_TIMEOUT_MS: '75',
|
|
63
|
-
LDM_STATUS_TOTAL_BUDGET_MS: '250',
|
|
64
|
-
},
|
|
65
|
-
});
|
|
79
|
+
const result = await runStatus({ home, registryUrl: `http://${address.address}:${address.port}` });
|
|
66
80
|
const elapsedMs = Date.now() - startedAt;
|
|
81
|
+
server.closeAllConnections();
|
|
82
|
+
server.close();
|
|
67
83
|
|
|
68
84
|
assert(result.status === 0, `ldm status exited ${result.status}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`);
|
|
69
85
|
assert(elapsedMs < 2500, `ldm status should return before the process timeout; elapsed ${elapsedMs}ms`);
|