@jacebenson/jsn 4.3.7-dev.35 → 4.3.7-dev.37
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/package.json +1 -1
- package/src/commands/midservers.js +64 -13
- package/src/midserver-direct.js +63 -3
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
3
4
|
import { input, password as passwordPrompt, select } from '@inquirer/prompts';
|
|
4
5
|
import { confirmDelete, formatRecordForDisplay, getStringField, interactiveList, canPrompt, assertSafeExactMatch } from '../helpers.js';
|
|
5
6
|
import { FormatAuto } from '../output.js';
|
|
6
7
|
import { declareCapabilities } from '../capabilities.js';
|
|
7
8
|
import { resolveRecord, unwrapSysId } from '../resolve-record.js';
|
|
8
9
|
import { detectDocker, defaultRun, installDockerMidServer, midInstallDirectory, normalizeMidName, validateMidCredentials } from '../midserver-install.js';
|
|
9
|
-
import { detectDirectPlatform, installDirectMidServer, readDirectInstallation, removeDirectInstallation, runDirectInstallation } from '../midserver-direct.js';
|
|
10
|
+
import { detectDirectPlatform, followDirectLog, installDirectMidServer, readDirectInstallation, removeDirectInstallation, resolveDirectLogFile, runDirectInstallation } from '../midserver-direct.js';
|
|
10
11
|
|
|
11
12
|
const AGENT_TABLE = 'ecc_agent';
|
|
12
13
|
const QUEUE_TABLE = 'ecc_queue';
|
|
@@ -171,6 +172,40 @@ function midDiagnosticsCommand(kind, wrap) {
|
|
|
171
172
|
};
|
|
172
173
|
}
|
|
173
174
|
|
|
175
|
+
// Progress for long, otherwise-silent direct-install phases. Human terminals
|
|
176
|
+
// only, and on stderr, so the stdout envelope and --json output stay clean.
|
|
177
|
+
export function directProgress(app) {
|
|
178
|
+
const human = app.output?.getFormat() === FormatAuto && (canPrompt() || process.stderr.isTTY === true);
|
|
179
|
+
if (!human) return () => {};
|
|
180
|
+
const mib = bytes => (Number(bytes || 0) / (1024 * 1024)).toFixed(0);
|
|
181
|
+
return event => {
|
|
182
|
+
if (event.phase === 'download-start') {
|
|
183
|
+
process.stderr.write(`Downloading the official MID ZIP from ServiceNow (about 532 MiB).\n ${event.archiveURL}\n`);
|
|
184
|
+
} else if (event.phase === 'download') {
|
|
185
|
+
const percent = event.total ? ` (${Math.floor((event.received / event.total) * 100)}%)` : '';
|
|
186
|
+
process.stderr.write(`\r ${mib(event.received)} MiB${percent} downloaded...`);
|
|
187
|
+
if (event.done) process.stderr.write('\n');
|
|
188
|
+
} else if (event.phase === 'extract-start') {
|
|
189
|
+
process.stderr.write(`Extracting ${event.entries} files into ${event.installDir}...\n`);
|
|
190
|
+
} else if (event.phase === 'extract-done') {
|
|
191
|
+
process.stderr.write('Extraction complete. Configuring the MID Server...\n');
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// A registration wait is minutes of silence otherwise; report elapsed progress.
|
|
197
|
+
export function registrationProgress(app, timeoutMs) {
|
|
198
|
+
const human = app.output?.getFormat() === FormatAuto && (canPrompt() || process.stderr.isTTY === true);
|
|
199
|
+
if (!human) return undefined;
|
|
200
|
+
const seconds = Math.round(timeoutMs / 1000);
|
|
201
|
+
let lastReport = 0;
|
|
202
|
+
return elapsedMs => {
|
|
203
|
+
if (elapsedMs - lastReport < 15000) return;
|
|
204
|
+
lastReport = elapsedMs;
|
|
205
|
+
process.stderr.write(`Waiting for ServiceNow to register the MID Server... ${Math.round(elapsedMs / 1000)}s of ${seconds}s\n`);
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
174
209
|
async function listAgents(app, argv) {
|
|
175
210
|
const columns = argv.columns
|
|
176
211
|
? [...new Set(argv.columns.split(',').map(column => column.trim()).filter(Boolean))]
|
|
@@ -266,10 +301,11 @@ async function promptMidCredentials(app, argv) {
|
|
|
266
301
|
}
|
|
267
302
|
}
|
|
268
303
|
|
|
269
|
-
export async function waitForMidServerUp(app, name, timeoutMs, intervalMs) {
|
|
304
|
+
export async function waitForMidServerUp(app, name, timeoutMs, intervalMs, onWait) {
|
|
270
305
|
const started = Date.now();
|
|
271
306
|
const fullName = name;
|
|
272
307
|
while (Date.now() - started <= timeoutMs) {
|
|
308
|
+
onWait?.(Date.now() - started);
|
|
273
309
|
const params = new URLSearchParams({
|
|
274
310
|
sysparm_query: `name=${fullName}`,
|
|
275
311
|
sysparm_limit: '1',
|
|
@@ -308,14 +344,13 @@ function removeLocalMidServer(instanceURL, name, directory) {
|
|
|
308
344
|
return true;
|
|
309
345
|
}
|
|
310
346
|
|
|
311
|
-
function showMidServerLogs(app, identifier, argv) {
|
|
347
|
+
async function showMidServerLogs(app, identifier, argv) {
|
|
312
348
|
const installDir = argv.directory || midInstallDirectory(app.getEffectiveInstance(), identifier);
|
|
313
349
|
const direct = readDirectInstallation(app.getEffectiveInstance(), identifier, argv.directory);
|
|
314
350
|
if (direct) {
|
|
315
351
|
if (!Number.isInteger(argv.tail) || argv.tail < 1 || argv.tail > 10000) throw new Error('Use --tail between 1 and 10000');
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
if (!fs.existsSync(logfile)) throw new Error('No MID agent log yet; the installation may only be prepared');
|
|
352
|
+
const logfile = resolveDirectLogFile(installDir);
|
|
353
|
+
if (!logfile) throw new Error('No MID agent log yet; the installation may only be prepared');
|
|
319
354
|
const stat = fs.lstatSync(logfile);
|
|
320
355
|
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('Untrusted MID log path');
|
|
321
356
|
const fd = fs.openSync(logfile, 'r');
|
|
@@ -324,8 +359,15 @@ function showMidServerLogs(app, identifier, argv) {
|
|
|
324
359
|
const length = Math.min(stat.size, 1024 * 1024);
|
|
325
360
|
const buffer = Buffer.alloc(length);
|
|
326
361
|
fs.readSync(fd, buffer, 0, length, stat.size - length);
|
|
327
|
-
|
|
362
|
+
const all = buffer.toString('utf8').split('\n');
|
|
363
|
+
if (all.length && all[all.length - 1] === '') all.pop();
|
|
364
|
+
text = all.slice(-argv.tail).join('\n');
|
|
328
365
|
} finally { fs.closeSync(fd); }
|
|
366
|
+
if (argv.follow) {
|
|
367
|
+
// Following streams new lines directly; the JSON envelope would be unreachable.
|
|
368
|
+
process.stdout.write(`Tailing ${logfile} (Ctrl+C to stop)\n${text}\n`);
|
|
369
|
+
return followDirectLog(installDir);
|
|
370
|
+
}
|
|
329
371
|
app.ok({ installation: 'direct', logfile, text, _formatted: `${text}\n` }, { summary: 'Local MID agent log (last 1 MiB maximum)' });
|
|
330
372
|
return;
|
|
331
373
|
}
|
|
@@ -377,10 +419,10 @@ export function midserversCmd(wrap) {
|
|
|
377
419
|
describe: 'Show local native or Docker Compose MID logs',
|
|
378
420
|
builder: (b) => b
|
|
379
421
|
.option('tail', { type: 'number', default: 100, describe: 'Number of log lines' })
|
|
380
|
-
.option('follow', { type: 'boolean', default: false, describe: 'Follow the logs
|
|
422
|
+
.option('follow', { type: 'boolean', default: false, describe: 'Follow the logs' })
|
|
381
423
|
.option('directory', { type: 'string', describe: 'Local installation directory override' }),
|
|
382
424
|
handler: wrap(async (argv, app) => {
|
|
383
|
-
showMidServerLogs(app, argv.identifier, argv);
|
|
425
|
+
await showMidServerLogs(app, argv.identifier, argv);
|
|
384
426
|
}),
|
|
385
427
|
})
|
|
386
428
|
.command({
|
|
@@ -388,10 +430,10 @@ export function midserversCmd(wrap) {
|
|
|
388
430
|
describe: 'Show local native or Docker Compose MID logs',
|
|
389
431
|
builder: (b) => b
|
|
390
432
|
.option('tail', { type: 'number', default: 100, describe: 'Number of log lines' })
|
|
391
|
-
.option('follow', { type: 'boolean', default: false, describe: 'Follow the logs
|
|
433
|
+
.option('follow', { type: 'boolean', default: false, describe: 'Follow the logs' })
|
|
392
434
|
.option('directory', { type: 'string', describe: 'Local installation directory override' }),
|
|
393
435
|
handler: wrap(async (argv, app) => {
|
|
394
|
-
showMidServerLogs(app, argv.identifier, argv);
|
|
436
|
+
await showMidServerLogs(app, argv.identifier, argv);
|
|
395
437
|
}),
|
|
396
438
|
})
|
|
397
439
|
.command({
|
|
@@ -472,15 +514,24 @@ export function midserversCmd(wrap) {
|
|
|
472
514
|
const existing = await app.sdk.list(AGENT_TABLE, new URLSearchParams({ sysparm_query: `name=${shortName}`, sysparm_fields: 'sys_id,name', sysparm_limit: '1' }));
|
|
473
515
|
if (existing.length) throw new Error(`MID '${shortName}' already exists in ServiceNow; choose a new name. Existing runtime and record were not changed.`);
|
|
474
516
|
const installation = await installDirectMidServer({
|
|
475
|
-
instanceURL: app.getEffectiveInstance(), ...credentials, name, directory: argv.directory,
|
|
517
|
+
instanceURL: app.getEffectiveInstance(), ...credentials, name, directory: argv.directory,
|
|
518
|
+
runNow: decision === 'start', onProgress: directProgress(app),
|
|
476
519
|
});
|
|
477
520
|
if (installation.state === 'prepared') {
|
|
478
521
|
app.ok(installation, { summary: `Prepared native MID '${installation.name}' in ${installation.installDir}; not started or registered. Start with jsn midservers start ${installation.name}${argv.directory ? ' --directory ' + JSON.stringify(installation.installDir) : ''}. Windows start runs in the foreground console; no service is installed.` });
|
|
479
522
|
return;
|
|
480
523
|
}
|
|
481
524
|
let record;
|
|
482
|
-
|
|
525
|
+
const directWait = Math.max(1, argv['wait-timeout']);
|
|
526
|
+
const directInterval = Math.max(500, argv['wait-interval']);
|
|
527
|
+
const onWait = registrationProgress(app, directWait);
|
|
528
|
+
const started = Date.now();
|
|
529
|
+
try {
|
|
530
|
+
record = await waitForMidServerUp(app, installation.name, directWait, directInterval, onWait);
|
|
531
|
+
onWait?.(directWait);
|
|
532
|
+
}
|
|
483
533
|
catch {
|
|
534
|
+
onWait?.(Math.max(directWait, Date.now() - started));
|
|
484
535
|
app.ok({ ...installation, registration: 'pending', validated: false }, { summary: `Native MID start command completed; registration is pending. Runtime retained for diagnostics at ${installation.installDir}; no forced teardown.` });
|
|
485
536
|
return;
|
|
486
537
|
}
|
package/src/midserver-direct.js
CHANGED
|
@@ -71,20 +71,28 @@ function checkParents(directory, platform) {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
export async function downloadMidZip(url, archive, fetchImpl = fetch) {
|
|
74
|
+
export async function downloadMidZip(url, archive, fetchImpl = fetch, onProgress) {
|
|
75
75
|
const response = await fetchImpl(url, { redirect: 'error', signal: AbortSignal.timeout(600000) });
|
|
76
76
|
if (!response.ok || !response.body) throw new Error(`MID ZIP download failed (HTTP ${response.status})`);
|
|
77
77
|
const length = Number(response.headers.get('content-length'));
|
|
78
78
|
if (length > MAX_ARCHIVE) throw new Error('MID ZIP exceeds 1 GiB download limit');
|
|
79
|
+
onProgress?.({ phase: 'download', received: 0, total: Number.isFinite(length) ? length : 0 });
|
|
79
80
|
const fd = fs.openSync(archive, 'wx', 0o600);
|
|
80
81
|
let size = 0;
|
|
82
|
+
let reported = 0;
|
|
83
|
+
const step = Math.max(4 * 1024 * 1024, Math.floor((Number.isFinite(length) ? length : MAX_ARCHIVE) / 100));
|
|
81
84
|
try {
|
|
82
85
|
for await (const chunk of response.body) {
|
|
83
86
|
size += chunk.length;
|
|
84
87
|
if (size > MAX_ARCHIVE) throw new Error('MID ZIP exceeds 1 GiB download limit');
|
|
85
88
|
fs.writeSync(fd, chunk);
|
|
89
|
+
if (onProgress && size - reported >= step) {
|
|
90
|
+
reported = size;
|
|
91
|
+
onProgress({ phase: 'download', received: size, total: Number.isFinite(length) ? length : 0 });
|
|
92
|
+
}
|
|
86
93
|
}
|
|
87
94
|
if (length && size !== length) throw new Error('Incomplete MID ZIP download');
|
|
95
|
+
onProgress?.({ phase: 'download', received: size, total: size, done: true });
|
|
88
96
|
} finally { fs.closeSync(fd); }
|
|
89
97
|
}
|
|
90
98
|
|
|
@@ -145,6 +153,55 @@ export function readDirectInstallation(instanceURL, name, directory) {
|
|
|
145
153
|
return metadata;
|
|
146
154
|
}
|
|
147
155
|
|
|
156
|
+
// The vendor rotates agent0.log to agent0.log.0 (and upward), so the plain
|
|
157
|
+
// name may not exist on a healthy install. Newest real file wins; links and
|
|
158
|
+
// non-files are never followed.
|
|
159
|
+
export function resolveDirectLogFile(installDir) {
|
|
160
|
+
const directory = path.join(installDir, 'agent', 'logs');
|
|
161
|
+
let names;
|
|
162
|
+
try { names = fs.readdirSync(directory); } catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
163
|
+
const candidates = [];
|
|
164
|
+
for (const name of names) {
|
|
165
|
+
const agent = /^agent0\.log(\.\d+)?$/.test(name);
|
|
166
|
+
if (!agent && name !== 'wrapper.log') continue;
|
|
167
|
+
const full = path.join(directory, name);
|
|
168
|
+
const stat = fs.lstatSync(full);
|
|
169
|
+
if (!stat.isFile() || stat.isSymbolicLink()) continue;
|
|
170
|
+
candidates.push({ full, agent, mtime: stat.mtimeMs });
|
|
171
|
+
}
|
|
172
|
+
candidates.sort((a, b) => (b.agent - a.agent) || (b.mtime - a.mtime));
|
|
173
|
+
return candidates.length ? candidates[0].full : null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Tail a direct install's agent log, following rotation.
|
|
178
|
+
*
|
|
179
|
+
* The vendor rotates by creating a new file, so the followed path can change
|
|
180
|
+
* between polls; a shrinking file is treated as truncation and re-read from
|
|
181
|
+
* the start. Each appended chunk is handed to `writer` unchanged.
|
|
182
|
+
*/
|
|
183
|
+
export async function followDirectLog(installDir, { writer = chunk => process.stdout.write(chunk), intervalMs = 1000, iterations = Infinity, sleep } = {}) {
|
|
184
|
+
const pause = sleep || (ms => new Promise(resolve => setTimeout(resolve, ms)));
|
|
185
|
+
let file = resolveDirectLogFile(installDir);
|
|
186
|
+
if (!file) throw new Error('No MID agent log yet; the installation may only be prepared');
|
|
187
|
+
let offset = fs.statSync(file).size;
|
|
188
|
+
for (let seen = 0; seen < iterations; seen++) {
|
|
189
|
+
await pause(intervalMs);
|
|
190
|
+
const next = resolveDirectLogFile(installDir) || file;
|
|
191
|
+
if (next !== file) { file = next; offset = 0; }
|
|
192
|
+
const stat = fs.lstatSync(file);
|
|
193
|
+
if (!stat.isFile() || stat.isSymbolicLink()) return;
|
|
194
|
+
if (stat.size < offset) offset = 0;
|
|
195
|
+
if (stat.size === offset) continue;
|
|
196
|
+
const length = stat.size - offset;
|
|
197
|
+
const buffer = Buffer.alloc(length);
|
|
198
|
+
const fd = fs.openSync(file, 'r');
|
|
199
|
+
try { fs.readSync(fd, buffer, 0, length, offset); } finally { fs.closeSync(fd); }
|
|
200
|
+
offset = stat.size;
|
|
201
|
+
writer(buffer.toString('utf8'));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
148
205
|
function uncertainState(metadata) {
|
|
149
206
|
return `MID '${metadata.name}' runtime state is uncertain after an interrupted lifecycle command. Confirm no vendor MID process from ${metadata.installDir} is running, then rerun; installed files are kept.`;
|
|
150
207
|
}
|
|
@@ -189,7 +246,7 @@ export function removeDirectInstallation(metadata, run = defaultRun) {
|
|
|
189
246
|
}
|
|
190
247
|
|
|
191
248
|
export async function installDirectMidServer({ instanceURL, username, password, name, directory, releaseName: suppliedRelease,
|
|
192
|
-
runNow = false, platform = process.platform, arch = process.arch, fetchImpl = fetch, download = downloadMidZip,
|
|
249
|
+
runNow = false, platform = process.platform, arch = process.arch, fetchImpl = fetch, download = downloadMidZip, onProgress,
|
|
193
250
|
protect = protectDirectory, run = defaultRun }) {
|
|
194
251
|
const native = detectDirectPlatform(platform, arch);
|
|
195
252
|
if (native === 'linux' && process.getuid?.() === 0) throw new Error('Run direct MID installation as a non-root user');
|
|
@@ -216,9 +273,12 @@ export async function installDirectMidServer({ instanceURL, username, password,
|
|
|
216
273
|
let prepared = false;
|
|
217
274
|
try {
|
|
218
275
|
protect(installDir, native);
|
|
219
|
-
|
|
276
|
+
onProgress?.({ phase: 'download-start', archiveURL, platform: native, installDir, name: shortName });
|
|
277
|
+
await download(archiveURL, archive, fetchImpl, onProgress);
|
|
220
278
|
const entries = inspectMidZip(archive);
|
|
279
|
+
onProgress?.({ phase: 'extract-start', entries: entries.length, installDir });
|
|
221
280
|
await extractMidZip(archive, installDir, entries);
|
|
281
|
+
onProgress?.({ phase: 'extract-done', entries: entries.length, installDir });
|
|
222
282
|
const agent = path.join(installDir, 'agent');
|
|
223
283
|
for (const filename of ['config.xml', 'conf/wrapper-override.conf', native === 'linux' ? 'bin/mid.sh' : 'bin/mid.bat']) {
|
|
224
284
|
if (!fs.statSync(path.join(agent, filename)).isFile()) throw new Error(`Vendor ZIP missing ${filename}`);
|