@gefyra/complies-with-eu 1.2.0 → 1.3.1
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/CHANGELOG.md +27 -0
- package/README.md +19 -10
- package/bin/complies-with-eu.js +11 -3
- package/package.json +7 -7
- package/src/commands/search.js +8 -3
- package/src/commands/validate.js +82 -45
- package/src/lib/constraintCheck.js +19 -0
- package/src/lib/javaValidator.js +131 -50
- package/src/lib/javaValidatorServer.js +166 -23
- package/src/lib/output.js +44 -0
- package/src/lib/preflight.js +1 -1
- package/src/lib/reportGen.js +39 -5
- package/src/lib/toolDownloader.js +3 -3
- package/src/lib/validateCore.js +13 -6
- package/src/lib/validatorOutput.js +276 -0
package/src/lib/javaValidator.js
CHANGED
|
@@ -7,6 +7,12 @@ const chalk = require('chalk');
|
|
|
7
7
|
const { getValidatorJar, loadValidatorParams } = require('./config');
|
|
8
8
|
const { getJavaCommand } = require('./javaCheck');
|
|
9
9
|
const { track } = require('./processRegistry');
|
|
10
|
+
const {
|
|
11
|
+
createOutputHandler,
|
|
12
|
+
createProgress,
|
|
13
|
+
extractResultInfo,
|
|
14
|
+
createValidatorError,
|
|
15
|
+
} = require('./validatorOutput');
|
|
10
16
|
|
|
11
17
|
/**
|
|
12
18
|
* Runs `fn` over `items` with at most `limit` concurrent calls and returns
|
|
@@ -29,12 +35,18 @@ async function mapSettled(items, limit, fn) {
|
|
|
29
35
|
return results;
|
|
30
36
|
}
|
|
31
37
|
|
|
38
|
+
// "*FAILURE*: ...", "*SUCCESS*: ..." or "Success: ..." (validator 7.x prints the
|
|
39
|
+
// success summary without asterisks). Anchored at column 0, so issue lines
|
|
40
|
+
// (indented) never match.
|
|
41
|
+
const SUMMARY_LINE = /^\*?(FAILURE|SUCCESS)\*?:\s*(.+)/i;
|
|
42
|
+
|
|
32
43
|
/**
|
|
33
44
|
* Parses the Java Validator console output into structured issues.
|
|
34
45
|
*
|
|
35
46
|
* Expected line formats:
|
|
36
47
|
* *FAILURE*: 6 errors, 18 warnings, 12 notes
|
|
37
|
-
* *SUCCESS*: ...
|
|
48
|
+
* *SUCCESS*: ... (older validators)
|
|
49
|
+
* Success: 0 errors, ... (validator 7.x when there are no errors)
|
|
38
50
|
* Severity @ Location (line X, colY): Message {MSG_ID}
|
|
39
51
|
* Severity @ Location: Message {MSG_ID}
|
|
40
52
|
* sub-detail line
|
|
@@ -50,9 +62,9 @@ function parseValidatorOutput(output) {
|
|
|
50
62
|
|
|
51
63
|
for (const line of lines) {
|
|
52
64
|
// Summary line
|
|
53
|
-
const summaryMatch = line.match(
|
|
65
|
+
const summaryMatch = line.match(SUMMARY_LINE);
|
|
54
66
|
if (summaryMatch) {
|
|
55
|
-
summary = { result: summaryMatch[1], text: summaryMatch[2].trim() };
|
|
67
|
+
summary = { result: summaryMatch[1].toUpperCase(), text: summaryMatch[2].trim() };
|
|
56
68
|
currentIssue = null;
|
|
57
69
|
continue;
|
|
58
70
|
}
|
|
@@ -163,23 +175,39 @@ function parseValidatorOutputBatch(output) {
|
|
|
163
175
|
}
|
|
164
176
|
|
|
165
177
|
/**
|
|
166
|
-
*
|
|
167
|
-
*
|
|
178
|
+
* Normalises the legacy boolean `debug` argument or an options object into
|
|
179
|
+
* { debug, verbose }. --debug implies verbose output.
|
|
180
|
+
*/
|
|
181
|
+
function normalizeRunOptions(opts) {
|
|
182
|
+
const o = (opts && typeof opts === 'object') ? opts : { debug: !!opts };
|
|
183
|
+
const debug = !!o.debug;
|
|
184
|
+
return { ...o, debug, verbose: debug || !!o.verbose };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Spawns the Java validator and collects its combined output. The output is
|
|
189
|
+
* mirrored only with `verbose` (to stderr, never to stdout, which is reserved
|
|
190
|
+
* for --json output); otherwise only phase hints are reported via `onPhase`.
|
|
168
191
|
*
|
|
169
|
-
* @
|
|
192
|
+
* @param {string[]} args
|
|
193
|
+
* @param {object} [opts]
|
|
194
|
+
* @param {boolean} [opts.verbose] - Stream the raw output
|
|
195
|
+
* @param {Function} [opts.onPhase] - Phase hint callback (see validatorOutput.createOutputHandler)
|
|
196
|
+
* @param {Function} [opts.onChunk] - Called with every raw output chunk
|
|
197
|
+
* @param {Function} [opts.write] - Sink for mirrored output (default: stderr)
|
|
198
|
+
* @param {Function} [opts.spawnFn] - Replacement for child_process.spawn (tests)
|
|
199
|
+
* @returns {Promise<{ code: number|null, signal: string|null, rawOutput: string }>}
|
|
170
200
|
*/
|
|
171
|
-
function spawnValidator(args) {
|
|
201
|
+
function spawnValidator(args, { verbose = false, onPhase, onChunk, write, spawnFn = spawn } = {}) {
|
|
172
202
|
return new Promise((resolve, reject) => {
|
|
173
|
-
const
|
|
174
|
-
const proc = track(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
proc.stderr.on('data', onData);
|
|
182
|
-
proc.on('close', (code) => resolve({ code, rawOutput: outputChunks.join('') }));
|
|
203
|
+
const handler = createOutputHandler({ verbose, write, onPhase, onChunk });
|
|
204
|
+
const proc = track(spawnFn(getJavaCommand(), args, { windowsHide: true }));
|
|
205
|
+
proc.stdout.on('data', handler.onData);
|
|
206
|
+
proc.stderr.on('data', handler.onData);
|
|
207
|
+
proc.on('close', (code, signal) => {
|
|
208
|
+
handler.flush();
|
|
209
|
+
resolve({ code, signal: signal || null, rawOutput: handler.getOutput() });
|
|
210
|
+
});
|
|
183
211
|
proc.on('error', (err) => reject(new Error(`Could not start Java for the FHIR validator: ${err.message}`)));
|
|
184
212
|
});
|
|
185
213
|
}
|
|
@@ -202,72 +230,125 @@ function printCommand(args, debug, label, igCount) {
|
|
|
202
230
|
}
|
|
203
231
|
}
|
|
204
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Runs the validator with concise progress (or streamed output with `verbose`),
|
|
235
|
+
* always writes the raw output to `logPath`, and rejects with an understandable
|
|
236
|
+
* error (exit code, output tail, log path) when `hasResult` says the output
|
|
237
|
+
* contains no usable result.
|
|
238
|
+
*
|
|
239
|
+
* @param {object} p
|
|
240
|
+
* @param {string[]} p.args
|
|
241
|
+
* @param {string} p.logPath
|
|
242
|
+
* @param {Function} p.hasResult - (rawOutput) => boolean
|
|
243
|
+
* @param {boolean} [p.batch] - Describe the result as a number of file results
|
|
244
|
+
* @param {boolean} [p.verbose]
|
|
245
|
+
* @param {NodeJS.WritableStream} [p.stream] - Progress/output stream (default stderr)
|
|
246
|
+
* @param {Function} [p.spawnFn] - Replacement for child_process.spawn (tests)
|
|
247
|
+
* @returns {Promise<{ code: number|null, rawOutput: string, logPath: string }>}
|
|
248
|
+
*/
|
|
249
|
+
async function executeValidator({ args, logPath, hasResult, batch = false, verbose = false, stream = process.stderr, spawnFn }) {
|
|
250
|
+
const progress = createProgress({ text: 'Running HL7 FHIR Validator', verbose, stream });
|
|
251
|
+
let result;
|
|
252
|
+
try {
|
|
253
|
+
result = await spawnValidator(args, {
|
|
254
|
+
verbose,
|
|
255
|
+
spawnFn,
|
|
256
|
+
write: (text) => stream.write(chalk.gray(text)),
|
|
257
|
+
onPhase: (label) => progress.phase(label),
|
|
258
|
+
});
|
|
259
|
+
} catch (err) {
|
|
260
|
+
progress.fail('HL7 FHIR Validator could not be started');
|
|
261
|
+
throw err;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const { code, signal, rawOutput } = result;
|
|
265
|
+
// Written after the run (not streamed): in batch mode the log lives inside the
|
|
266
|
+
// directory being validated and must not exist while the validator scans it.
|
|
267
|
+
let logWritten = true;
|
|
268
|
+
try {
|
|
269
|
+
await fs.outputFile(logPath, rawOutput, 'utf8');
|
|
270
|
+
} catch {
|
|
271
|
+
logWritten = false; // best effort; never fail the run because of the log
|
|
272
|
+
}
|
|
273
|
+
const shownLog = logWritten ? logPath : null;
|
|
274
|
+
if (!hasResult(rawOutput)) {
|
|
275
|
+
progress.fail(signal
|
|
276
|
+
? `HL7 FHIR Validator was terminated (${signal})`
|
|
277
|
+
: `HL7 FHIR Validator failed (exit code ${code}, no result summary)`);
|
|
278
|
+
throw createValidatorError({ reason: 'produced no result summary', code, signal, rawOutput, logPath: shownLog });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const { summaryLine, doneLine, summaryCount } = extractResultInfo(rawOutput);
|
|
282
|
+
progress.succeed(batch
|
|
283
|
+
? `HL7 FHIR Validator finished: ${summaryCount} file result(s)`
|
|
284
|
+
: `HL7 FHIR Validator finished: ${summaryLine}`);
|
|
285
|
+
if (doneLine) stream.write(chalk.gray(` ${doneLine}\n`));
|
|
286
|
+
if (shownLog) stream.write(chalk.gray(` Raw validator output: ${shownLog}\n`));
|
|
287
|
+
return { code, rawOutput, logPath: shownLog };
|
|
288
|
+
}
|
|
289
|
+
|
|
205
290
|
/**
|
|
206
291
|
* Runs the FHIR Java Validator against a resource file.
|
|
207
292
|
* Does NOT use -output / -html-output; parses the console output instead.
|
|
293
|
+
* The raw output is always written to "<resource>-raw.log" next to the file.
|
|
208
294
|
*
|
|
209
295
|
* Rejects when the validator produced no result summary (crash, wrong Java,
|
|
210
296
|
* unknown option, ...): a missing summary must never be read as "no issues".
|
|
211
297
|
*
|
|
212
298
|
* @param {string} resourceFile - Path to the resource/profile JSON to validate
|
|
213
299
|
* @param {string[]} igIds - List of name#version identifiers for -ig
|
|
214
|
-
* @param {boolean}
|
|
300
|
+
* @param {boolean|{debug?: boolean, verbose?: boolean}} [opts] - Legacy debug flag or options
|
|
215
301
|
* @param {string[]} extraArgs - Additional arguments appended after base params (e.g. ['-resolve-path', dir])
|
|
216
|
-
* @returns {Promise<{rawOutput: string, parsed: object, mode: 'local'}>}
|
|
302
|
+
* @returns {Promise<{rawOutput: string, parsed: object, mode: 'local', logPath: string}>}
|
|
217
303
|
*/
|
|
218
|
-
async function runValidator(resourceFile, igIds,
|
|
219
|
-
|
|
304
|
+
async function runValidator(resourceFile, igIds, opts = false, extraArgs = []) {
|
|
305
|
+
const { debug, verbose } = normalizeRunOptions(opts);
|
|
220
306
|
const args = await buildLocalArgs(resourceFile, igIds, extraArgs);
|
|
221
307
|
printCommand(args, debug, '', igIds.length);
|
|
222
308
|
|
|
223
|
-
const { code, rawOutput } = await spawnValidator(args);
|
|
224
|
-
const parsed = parseValidatorOutput(rawOutput);
|
|
225
309
|
const logPath = resourceFile.replace(/\.json$/, '') + '-raw.log';
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
`Raw output: ${logPath}`
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
return { rawOutput, parsed, mode: 'local' };
|
|
310
|
+
const { rawOutput } = await executeValidator({
|
|
311
|
+
args,
|
|
312
|
+
logPath,
|
|
313
|
+
verbose,
|
|
314
|
+
hasResult: (out) => !!parseValidatorOutput(out).summary,
|
|
315
|
+
});
|
|
316
|
+
return { rawOutput, parsed: parseValidatorOutput(rawOutput), mode: 'local', logPath };
|
|
237
317
|
}
|
|
238
318
|
|
|
239
319
|
/**
|
|
240
320
|
* Runs the FHIR Java Validator against an entire directory in one invocation.
|
|
241
|
-
* Parses per-file sections from the combined output.
|
|
321
|
+
* Parses per-file sections from the combined output. The raw output is always
|
|
322
|
+
* written to "<dir>/_batch-raw.log".
|
|
242
323
|
*
|
|
243
324
|
* @param {string} dirPath - Directory containing the resource files to validate
|
|
244
325
|
* @param {string[]} igIds - List of name#version identifiers for -ig
|
|
245
|
-
* @param {boolean
|
|
326
|
+
* @param {boolean|{debug?: boolean, verbose?: boolean}} [opts] - Legacy debug flag or options
|
|
246
327
|
* @param {string[]} extraArgs - Additional arguments appended after base params
|
|
247
|
-
* @returns {Promise<{rawOutput: string, results: Array<{fileName, summary, issues}
|
|
328
|
+
* @returns {Promise<{rawOutput: string, results: Array<{fileName, summary, issues}>, logPath: string}>}
|
|
248
329
|
*/
|
|
249
|
-
async function runValidatorBatch(dirPath, igIds,
|
|
250
|
-
|
|
330
|
+
async function runValidatorBatch(dirPath, igIds, opts = false, extraArgs = []) {
|
|
331
|
+
const { debug, verbose } = normalizeRunOptions(opts);
|
|
251
332
|
const args = await buildLocalArgs(dirPath, igIds, extraArgs);
|
|
252
333
|
printCommand(args, debug, ' (batch)', igIds.length);
|
|
253
334
|
|
|
254
|
-
const { code, rawOutput } = await spawnValidator(args);
|
|
255
|
-
const results = parseValidatorOutputBatch(rawOutput);
|
|
256
335
|
const logPath = path.join(dirPath, '_batch-raw.log');
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
return { rawOutput, results };
|
|
336
|
+
const { rawOutput } = await executeValidator({
|
|
337
|
+
args,
|
|
338
|
+
logPath,
|
|
339
|
+
verbose,
|
|
340
|
+
batch: true,
|
|
341
|
+
hasResult: (out) => parseValidatorOutputBatch(out).some(r => r.summary),
|
|
342
|
+
});
|
|
343
|
+
return { rawOutput, results: parseValidatorOutputBatch(rawOutput), logPath };
|
|
266
344
|
}
|
|
267
345
|
|
|
268
346
|
module.exports = {
|
|
269
347
|
runValidator,
|
|
270
348
|
runValidatorBatch,
|
|
349
|
+
executeValidator,
|
|
350
|
+
spawnValidator,
|
|
351
|
+
normalizeRunOptions,
|
|
271
352
|
parseValidatorOutput,
|
|
272
353
|
parseValidatorOutputBatch,
|
|
273
354
|
mapSettled,
|
|
@@ -10,12 +10,15 @@ const { getValidatorJar, loadValidatorParams } = require('./config');
|
|
|
10
10
|
const { getJavaCommand } = require('./javaCheck');
|
|
11
11
|
const { track } = require('./processRegistry');
|
|
12
12
|
const { mapSettled } = require('./javaValidator');
|
|
13
|
+
const { createOutputHandler, createProgress, formatValidatorError, formatValidatorErrorShort, createValidatorError, tailLines } = require('./validatorOutput');
|
|
13
14
|
|
|
14
15
|
const DEFAULT_PORT = 7272;
|
|
15
16
|
const READY_POLL_INTERVAL_MS = 2500;
|
|
16
17
|
// Total budget: port open + lazy IG loading + possible network timeouts on first request
|
|
17
18
|
const READY_TIMEOUT_MS = 600_000; // 10 min
|
|
18
19
|
const POST_CONCURRENCY = 4;
|
|
20
|
+
// In-memory tail of the server output kept for error messages (the full output goes to the log file)
|
|
21
|
+
const SERVER_OUTPUT_TAIL_CHARS = 64 * 1024;
|
|
19
22
|
// Text of the validator message SD_EXTENSION_COMPLIES_WITH_ERROR
|
|
20
23
|
const COMPLIES_WITH_ERROR_TEXT = /does not comply with claimed profile '[^']+'/;
|
|
21
24
|
|
|
@@ -70,11 +73,11 @@ async function serverHasCanonical(port, canonical) {
|
|
|
70
73
|
* Phase 1: polls until the port accepts connections.
|
|
71
74
|
* Throws if `isDead()` returns true (process exited).
|
|
72
75
|
*/
|
|
73
|
-
async function waitForPort(port, deadline, isDead) {
|
|
76
|
+
async function waitForPort(port, deadline, isDead, pollIntervalMs = READY_POLL_INTERVAL_MS) {
|
|
74
77
|
while (Date.now() < deadline) {
|
|
75
78
|
if (isDead()) throw new Error('Validator process exited before the server became ready');
|
|
76
79
|
if (await isServerReady(port)) return;
|
|
77
|
-
await new Promise(r => setTimeout(r,
|
|
80
|
+
await new Promise(r => setTimeout(r, Math.max(0, Math.min(pollIntervalMs, deadline - Date.now()))));
|
|
78
81
|
}
|
|
79
82
|
throw new Error(`Validator server on port ${port} did not start within the timeout`);
|
|
80
83
|
}
|
|
@@ -115,46 +118,98 @@ async function warmupServer(port, deadline, isDead) {
|
|
|
115
118
|
* Waits until the server is truly ready (lazy loading complete) before returning.
|
|
116
119
|
* The JVM is killed if startup or warmup fails, and when the CLI exits.
|
|
117
120
|
*
|
|
121
|
+
* The server output is written to `logFile` (when given) and streamed to stderr
|
|
122
|
+
* only with `verbose`/`debug`; otherwise a concise progress indicator is shown.
|
|
123
|
+
* Startup failures include the last lines of server output and the log path.
|
|
124
|
+
*
|
|
118
125
|
* @param {object} opts
|
|
119
|
-
* @param {string[]} opts.igIds
|
|
120
|
-
* @param {number} [opts.port]
|
|
121
|
-
* @param {boolean} [opts.debug]
|
|
122
|
-
* @
|
|
126
|
+
* @param {string[]} opts.igIds - IG identifiers (name#version or local path) to pre-load
|
|
127
|
+
* @param {number} [opts.port] - Port to listen on (default: a free port chosen by the OS)
|
|
128
|
+
* @param {boolean} [opts.debug] - Print the full startup command (implies verbose)
|
|
129
|
+
* @param {boolean} [opts.verbose] - Stream the full server output to stderr
|
|
130
|
+
* @param {string} [opts.logFile] - File that receives the raw server output
|
|
131
|
+
* @param {NodeJS.WritableStream} [opts.stream] - Progress / output sink (default process.stderr)
|
|
132
|
+
* @param {Function} [opts.spawnFn] - child_process.spawn replacement (tests)
|
|
133
|
+
* @param {string} [opts.jar] - validator_cli.jar path (default: the installed one)
|
|
134
|
+
* @param {number} [opts.readyTimeoutMs] - Startup + warmup budget (default 10 min)
|
|
135
|
+
* @param {number} [opts.pollIntervalMs] - Port poll interval
|
|
136
|
+
* @param {number} [opts.startupGraceMs] - Pause that catches immediate start failures
|
|
137
|
+
* @returns {Promise<{ port: number, stop: Function, logFile: string|null,
|
|
138
|
+
* isDead: Function, exitInfo: Function, getOutput: Function, tail: Function }>}
|
|
139
|
+
* `isDead()` / `exitInfo()` tell whether (and how) the JVM exited after startup,
|
|
140
|
+
* `getOutput()` returns the in-memory tail of its output, `tail(n)` its last n lines.
|
|
123
141
|
*/
|
|
124
|
-
async function startValidatorServer({
|
|
125
|
-
|
|
142
|
+
async function startValidatorServer({
|
|
143
|
+
igIds,
|
|
144
|
+
port = null,
|
|
145
|
+
debug = false,
|
|
146
|
+
verbose = false,
|
|
147
|
+
logFile = null,
|
|
148
|
+
stream = process.stderr,
|
|
149
|
+
spawnFn = spawn,
|
|
150
|
+
jar = getValidatorJar(),
|
|
151
|
+
readyTimeoutMs = READY_TIMEOUT_MS,
|
|
152
|
+
pollIntervalMs = READY_POLL_INTERVAL_MS,
|
|
153
|
+
startupGraceMs = 600,
|
|
154
|
+
}) {
|
|
126
155
|
if (!await fs.pathExists(jar)) {
|
|
127
156
|
throw new Error(`validator_cli.jar not found at ${jar}. Run: complies-with-eu init`);
|
|
128
157
|
}
|
|
129
158
|
const listenPort = port || await findFreePort();
|
|
159
|
+
const showOutput = debug || verbose;
|
|
130
160
|
|
|
131
161
|
const baseParams = loadValidatorParams();
|
|
132
162
|
const igArgs = igIds.flatMap(id => ['-ig', id]);
|
|
133
163
|
|
|
134
164
|
const args = [
|
|
135
165
|
'-jar', jar,
|
|
136
|
-
'
|
|
166
|
+
'server', String(listenPort),
|
|
137
167
|
...igArgs,
|
|
138
168
|
...baseParams,
|
|
139
169
|
];
|
|
140
170
|
|
|
171
|
+
const write = (text) => stream.write(text);
|
|
141
172
|
if (debug) {
|
|
142
|
-
|
|
143
|
-
|
|
173
|
+
write(chalk.bold.yellow('\n[DEBUG] Starting validator server:\n'));
|
|
174
|
+
write(chalk.yellow(`${getJavaCommand()} ${args.join(' ')}\n\n`));
|
|
144
175
|
} else {
|
|
145
|
-
|
|
176
|
+
write(chalk.gray(` java -jar validator_cli.jar server ${listenPort} ... (${igIds.length} IGs)\n`));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let logStream = null;
|
|
180
|
+
if (logFile) {
|
|
181
|
+
await fs.ensureDir(path.dirname(logFile));
|
|
182
|
+
logStream = fs.createWriteStream(logFile, { encoding: 'utf8' });
|
|
183
|
+
logStream.on('error', () => { /* the log is best effort */ });
|
|
146
184
|
}
|
|
147
185
|
|
|
148
|
-
const
|
|
186
|
+
const progress = createProgress({ text: 'Starting HL7 FHIR Validator server', verbose: showOutput, stream });
|
|
187
|
+
let starting = true;
|
|
188
|
+
const handler = createOutputHandler({
|
|
189
|
+
verbose: showOutput,
|
|
190
|
+
write: (text) => write(chalk.gray(text)),
|
|
191
|
+
maxChars: SERVER_OUTPUT_TAIL_CHARS,
|
|
192
|
+
onPhase: (label) => { if (starting) progress.phase(label); },
|
|
193
|
+
onChunk: (text) => { if (logStream) logStream.write(text); },
|
|
194
|
+
});
|
|
149
195
|
|
|
150
|
-
proc
|
|
151
|
-
proc.
|
|
196
|
+
const proc = track(spawnFn(getJavaCommand(), args, { windowsHide: true }));
|
|
197
|
+
proc.stdout.on('data', handler.onData);
|
|
198
|
+
proc.stderr.on('data', handler.onData);
|
|
152
199
|
|
|
153
200
|
let procExited = false;
|
|
154
201
|
let procError = null;
|
|
202
|
+
let exitCode = null;
|
|
203
|
+
let exitSignal = null;
|
|
155
204
|
proc.on('error', err => { procError = err; });
|
|
156
|
-
proc.on('exit', () => {
|
|
205
|
+
proc.on('exit', (code, signal) => {
|
|
206
|
+
procExited = true;
|
|
207
|
+
exitCode = code;
|
|
208
|
+
exitSignal = signal || null;
|
|
209
|
+
});
|
|
210
|
+
proc.on('close', () => { if (logStream) logStream.end(); });
|
|
157
211
|
|
|
212
|
+
const isDead = () => procExited || !!procError;
|
|
158
213
|
const stop = () => {
|
|
159
214
|
if (!procExited) {
|
|
160
215
|
try { proc.kill(); } catch { /* ignore */ }
|
|
@@ -163,22 +218,109 @@ async function startValidatorServer({ igIds, port = null, debug = false }) {
|
|
|
163
218
|
|
|
164
219
|
try {
|
|
165
220
|
// Brief pause to catch immediate startup failures (bad jar path, wrong Java, etc.)
|
|
166
|
-
await new Promise(r => setTimeout(r,
|
|
221
|
+
await new Promise(r => setTimeout(r, startupGraceMs));
|
|
167
222
|
if (procError) throw new Error(`Failed to start validator server: ${procError.message}`);
|
|
168
223
|
|
|
169
|
-
const deadline = Date.now() +
|
|
170
|
-
const isDead = () => procExited || !!procError;
|
|
224
|
+
const deadline = Date.now() + readyTimeoutMs;
|
|
171
225
|
|
|
172
|
-
await waitForPort(listenPort, deadline, isDead);
|
|
173
|
-
|
|
226
|
+
await waitForPort(listenPort, deadline, isDead, pollIntervalMs);
|
|
227
|
+
if (showOutput) {
|
|
228
|
+
write(chalk.gray(` Validator server accepting connections on port ${listenPort}, warming up...\n`));
|
|
229
|
+
} else {
|
|
230
|
+
progress.phase('Warming up');
|
|
231
|
+
}
|
|
174
232
|
|
|
175
233
|
await warmupServer(listenPort, deadline, isDead);
|
|
176
234
|
} catch (err) {
|
|
235
|
+
starting = false;
|
|
177
236
|
stop();
|
|
178
|
-
|
|
237
|
+
progress.fail('HL7 FHIR Validator server could not be started');
|
|
238
|
+
if (procError) throw err;
|
|
239
|
+
handler.flush();
|
|
240
|
+
throw createValidatorError({
|
|
241
|
+
subject: 'FHIR Java Validator server',
|
|
242
|
+
reason: `did not become ready (${err.message})`,
|
|
243
|
+
code: procExited ? exitCode : null,
|
|
244
|
+
signal: exitSignal,
|
|
245
|
+
rawOutput: handler.getOutput(),
|
|
246
|
+
logPath: logFile,
|
|
247
|
+
}, { cause: err });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
starting = false;
|
|
251
|
+
progress.succeed(`HL7 FHIR Validator server ready on port ${listenPort}`);
|
|
252
|
+
if (logFile) write(chalk.gray(` Server output: ${logFile}\n`));
|
|
253
|
+
return {
|
|
254
|
+
port: listenPort,
|
|
255
|
+
stop,
|
|
256
|
+
logFile,
|
|
257
|
+
isDead,
|
|
258
|
+
exitInfo: () => ({
|
|
259
|
+
exited: isDead(),
|
|
260
|
+
code: procExited ? exitCode : null,
|
|
261
|
+
signal: exitSignal,
|
|
262
|
+
error: procError ? procError.message : null,
|
|
263
|
+
}),
|
|
264
|
+
getOutput: () => { handler.flush(); return handler.getOutput(); },
|
|
265
|
+
tail: (n) => tailLines(handler.getOutput(), n),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Checks the results of a server batch for failed requests and, if there are
|
|
271
|
+
* any, explains them, so that a crashed or hung server is understandable
|
|
272
|
+
* without --verbose. A short grace period lets the JVM exit be noticed when the
|
|
273
|
+
* requests failed because it has just died.
|
|
274
|
+
*
|
|
275
|
+
* @param {object} server - Return value of startValidatorServer, or a reused
|
|
276
|
+
* server `{ port, stop }` without process information
|
|
277
|
+
* @param {Array<{error?: string}>} results - validateBatchViaServer results
|
|
278
|
+
* @param {object} [opts]
|
|
279
|
+
* @param {number} [opts.exitGraceMs] - How long to wait for a pending exit
|
|
280
|
+
* @returns {Promise<null|{ failed: number, total: number, dead: boolean, message: string, short: string }>}
|
|
281
|
+
* null when every request got a result. `message` is the full explanation
|
|
282
|
+
* (exit code, output tail, log path) for the console; `short` is one line for
|
|
283
|
+
* per-profile errors, the JSON output and reports.
|
|
284
|
+
*/
|
|
285
|
+
async function describeBatchFailure(server, results, { exitGraceMs = 1000 } = {}) {
|
|
286
|
+
const total = results.length;
|
|
287
|
+
const failed = results.filter(r => r && r.error).length;
|
|
288
|
+
if (failed === 0) return null;
|
|
289
|
+
|
|
290
|
+
const canInspect = typeof server.isDead === 'function';
|
|
291
|
+
if (canInspect && !server.isDead() && exitGraceMs > 0) {
|
|
292
|
+
const until = Date.now() + exitGraceMs;
|
|
293
|
+
while (!server.isDead() && Date.now() < until) await new Promise(r => setTimeout(r, 50));
|
|
179
294
|
}
|
|
295
|
+
const dead = canInspect && server.isDead();
|
|
296
|
+
const info = canInspect ? server.exitInfo() : { code: null, signal: null };
|
|
297
|
+
const logPath = server.logFile || null;
|
|
298
|
+
|
|
299
|
+
let reason;
|
|
300
|
+
if (dead) reason = 'did not finish the batch validation';
|
|
301
|
+
else if (failed === total) reason = `returned no result for any of the ${total} profile(s)`;
|
|
302
|
+
else reason = `returned no result for ${failed} of ${total} profile(s)`;
|
|
303
|
+
|
|
304
|
+
const message = canInspect
|
|
305
|
+
? formatValidatorError({
|
|
306
|
+
subject: 'FHIR Java Validator server',
|
|
307
|
+
reason,
|
|
308
|
+
code: info.code,
|
|
309
|
+
signal: info.signal,
|
|
310
|
+
rawOutput: server.getOutput(),
|
|
311
|
+
logPath,
|
|
312
|
+
})
|
|
313
|
+
: `FHIR Java Validator server ${reason}.`;
|
|
314
|
+
|
|
315
|
+
const short = formatValidatorErrorShort({
|
|
316
|
+
subject: 'FHIR Java Validator server',
|
|
317
|
+
reason,
|
|
318
|
+
code: dead ? info.code : null,
|
|
319
|
+
signal: dead ? info.signal : null,
|
|
320
|
+
logPath,
|
|
321
|
+
});
|
|
180
322
|
|
|
181
|
-
return {
|
|
323
|
+
return { failed, total, dead, message, short };
|
|
182
324
|
}
|
|
183
325
|
|
|
184
326
|
/**
|
|
@@ -313,4 +455,5 @@ module.exports = {
|
|
|
313
455
|
parseOperationOutcome,
|
|
314
456
|
validateViaServer,
|
|
315
457
|
validateBatchViaServer,
|
|
458
|
+
describeBatchFailure,
|
|
316
459
|
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Output streams of the CLI.
|
|
5
|
+
*
|
|
6
|
+
* - Progress (status lines, lists, spinners from ora) always goes to stderr, so
|
|
7
|
+
* all human progress output shares one stream and keeps its order when stdout
|
|
8
|
+
* and stderr are captured separately (e.g. Docker logs, CI) or merged.
|
|
9
|
+
* - The final result (e.g. the PASS/FAIL summary and the report path) goes to
|
|
10
|
+
* stdout, unless --json is set: then stdout carries only the JSON document and
|
|
11
|
+
* the human result goes to stderr as well.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// eslint-disable-next-line no-control-regex
|
|
15
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* chalk decides about colors based on stdout. When stdout is a terminal but
|
|
19
|
+
* stderr is redirected, strip the color codes from stderr lines so log files
|
|
20
|
+
* stay readable. FORCE_COLOR keeps the colors.
|
|
21
|
+
*/
|
|
22
|
+
function stripColorsForStream(stream, text) {
|
|
23
|
+
if (stream.isTTY || process.env.FORCE_COLOR) return text;
|
|
24
|
+
return text.replace(ANSI_RE, '');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Returns a console.log-like writer for the given stream (args joined with a space). */
|
|
28
|
+
function writerFor(stream) {
|
|
29
|
+
return (...args) => {
|
|
30
|
+
stream.write(stripColorsForStream(stream, args.join(' ')) + '\n');
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Progress logger: always stderr (same stream as the ora spinners). */
|
|
35
|
+
function makeProgressLogger() {
|
|
36
|
+
return writerFor(process.stderr);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Result logger: stdout, or stderr with --json (stdout is reserved for the JSON document). */
|
|
40
|
+
function makeResultLogger(options = {}) {
|
|
41
|
+
return options.json ? writerFor(process.stderr) : writerFor(process.stdout);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { makeProgressLogger, makeResultLogger, stripColorsForStream };
|
package/src/lib/preflight.js
CHANGED
|
@@ -13,7 +13,7 @@ const { ensureJava, requiredJavaMajor } = require('./javaCheck');
|
|
|
13
13
|
const { resolvePackageDir, packageHasSnapshots, downloadFhirPackage } = require('./fhirPkgTool');
|
|
14
14
|
const { CliError } = require('./errors');
|
|
15
15
|
|
|
16
|
-
const stderrLog = (
|
|
16
|
+
const stderrLog = require('./output').makeProgressLogger();
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Lists what `init` should have installed but is missing (pure-ish; no Java needed).
|