@ubercode/dcmtk 0.3.0 → 0.5.0
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 +4 -1
- package/dist/{DicomInstance-D9plqHp5.d.ts → DicomInstance-CGBr3a-C.d.ts} +1 -1
- package/dist/{DicomInstance-DNHPkkzl.d.cts → DicomInstance-DWOjhccQ.d.cts} +1 -1
- package/dist/{dcmodify-Gds9u5Vj.d.cts → dcmodify-B9js5K1f.d.cts} +2 -0
- package/dist/{dcmodify-B-_uUIKB.d.ts → dcmodify-BvaIeyJg.d.ts} +2 -0
- package/dist/dicom.cjs +27 -11
- package/dist/dicom.cjs.map +1 -1
- package/dist/dicom.d.cts +3 -3
- package/dist/dicom.d.ts +3 -3
- package/dist/dicom.js +27 -11
- package/dist/dicom.js.map +1 -1
- package/dist/index-CsYd7SMF.d.ts +1735 -0
- package/dist/index-DbUfqT7o.d.cts +1735 -0
- package/dist/index.cjs +896 -179
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -4
- package/dist/index.d.ts +5 -4
- package/dist/index.js +896 -179
- package/dist/index.js.map +1 -1
- package/dist/servers.cjs +157 -22
- package/dist/servers.cjs.map +1 -1
- package/dist/servers.d.cts +6 -1654
- package/dist/servers.d.ts +6 -1654
- package/dist/servers.js +157 -22
- package/dist/servers.js.map +1 -1
- package/dist/tools.cjs +754 -156
- package/dist/tools.cjs.map +1 -1
- package/dist/tools.d.cts +226 -10
- package/dist/tools.d.ts +226 -10
- package/dist/tools.js +754 -156
- package/dist/tools.js.map +1 -1
- package/package.json +17 -1
package/dist/servers.cjs
CHANGED
|
@@ -2201,19 +2201,28 @@ function repairJson(raw) {
|
|
|
2201
2201
|
var Dcm2jsonOptionsSchema = zod.z.object({
|
|
2202
2202
|
timeoutMs: zod.z.number().int().positive().optional(),
|
|
2203
2203
|
signal: zod.z.instanceof(AbortSignal).optional(),
|
|
2204
|
-
directOnly: zod.z.boolean().optional()
|
|
2204
|
+
directOnly: zod.z.boolean().optional(),
|
|
2205
|
+
verbosity: zod.z.enum(["verbose", "debug"]).optional()
|
|
2205
2206
|
}).strict().optional();
|
|
2206
|
-
|
|
2207
|
+
var VERBOSITY_FLAGS = { verbose: "-v", debug: "-d" };
|
|
2208
|
+
function buildVerbosityArgs(verbosity) {
|
|
2209
|
+
if (verbosity !== void 0) {
|
|
2210
|
+
return [VERBOSITY_FLAGS[verbosity]];
|
|
2211
|
+
}
|
|
2212
|
+
return [];
|
|
2213
|
+
}
|
|
2214
|
+
async function tryXmlPath(inputPath, timeoutMs, signal, verbosity) {
|
|
2207
2215
|
const xmlBinary = resolveBinary("dcm2xml");
|
|
2208
2216
|
if (!xmlBinary.ok) {
|
|
2209
2217
|
return err(xmlBinary.error);
|
|
2210
2218
|
}
|
|
2211
|
-
const
|
|
2219
|
+
const xmlArgs = [...buildVerbosityArgs(verbosity), "-nat", inputPath];
|
|
2220
|
+
const xmlResult = await execCommand(xmlBinary.value, xmlArgs, { timeoutMs, signal });
|
|
2212
2221
|
if (!xmlResult.ok) {
|
|
2213
2222
|
return err(xmlResult.error);
|
|
2214
2223
|
}
|
|
2215
2224
|
if (xmlResult.value.exitCode !== 0) {
|
|
2216
|
-
return err(createToolError("dcm2xml",
|
|
2225
|
+
return err(createToolError("dcm2xml", xmlArgs, xmlResult.value.exitCode, xmlResult.value.stderr));
|
|
2217
2226
|
}
|
|
2218
2227
|
const jsonResult = xmlToJson(xmlResult.value.stdout);
|
|
2219
2228
|
if (!jsonResult.ok) {
|
|
@@ -2221,17 +2230,18 @@ async function tryXmlPath(inputPath, timeoutMs, signal) {
|
|
|
2221
2230
|
}
|
|
2222
2231
|
return ok({ data: jsonResult.value, source: "xml" });
|
|
2223
2232
|
}
|
|
2224
|
-
async function tryDirectPath(inputPath, timeoutMs, signal) {
|
|
2233
|
+
async function tryDirectPath(inputPath, timeoutMs, signal, verbosity) {
|
|
2225
2234
|
const jsonBinary = resolveBinary("dcm2json");
|
|
2226
2235
|
if (!jsonBinary.ok) {
|
|
2227
2236
|
return err(jsonBinary.error);
|
|
2228
2237
|
}
|
|
2229
|
-
const
|
|
2238
|
+
const directArgs = [...buildVerbosityArgs(verbosity), inputPath];
|
|
2239
|
+
const result = await execCommand(jsonBinary.value, directArgs, { timeoutMs, signal });
|
|
2230
2240
|
if (!result.ok) {
|
|
2231
2241
|
return err(result.error);
|
|
2232
2242
|
}
|
|
2233
2243
|
if (result.value.exitCode !== 0) {
|
|
2234
|
-
return err(createToolError("dcm2json",
|
|
2244
|
+
return err(createToolError("dcm2json", directArgs, result.value.exitCode, result.value.stderr));
|
|
2235
2245
|
}
|
|
2236
2246
|
try {
|
|
2237
2247
|
const repaired = repairJson(result.value.stdout);
|
|
@@ -2248,20 +2258,22 @@ async function dcm2json(inputPath, options) {
|
|
|
2248
2258
|
}
|
|
2249
2259
|
const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
2250
2260
|
const signal = options?.signal;
|
|
2261
|
+
const verbosity = options?.verbosity;
|
|
2251
2262
|
if (options?.directOnly === true) {
|
|
2252
|
-
return tryDirectPath(inputPath, timeoutMs, signal);
|
|
2263
|
+
return tryDirectPath(inputPath, timeoutMs, signal, verbosity);
|
|
2253
2264
|
}
|
|
2254
|
-
const xmlResult = await tryXmlPath(inputPath, timeoutMs, signal);
|
|
2265
|
+
const xmlResult = await tryXmlPath(inputPath, timeoutMs, signal, verbosity);
|
|
2255
2266
|
if (xmlResult.ok) {
|
|
2256
2267
|
return xmlResult;
|
|
2257
2268
|
}
|
|
2258
|
-
return tryDirectPath(inputPath, timeoutMs, signal);
|
|
2269
|
+
return tryDirectPath(inputPath, timeoutMs, signal, verbosity);
|
|
2259
2270
|
}
|
|
2260
2271
|
var TAG_OR_PATH_PATTERN = /^\([0-9A-Fa-f]{4},[0-9A-Fa-f]{4}\)(\[\d+\](\.\([0-9A-Fa-f]{4},[0-9A-Fa-f]{4}\)(\[\d+\])?)*)?$/;
|
|
2261
2272
|
var TagModificationSchema = zod.z.object({
|
|
2262
2273
|
tag: zod.z.string().regex(TAG_OR_PATH_PATTERN),
|
|
2263
2274
|
value: zod.z.string()
|
|
2264
2275
|
});
|
|
2276
|
+
var VERBOSITY_FLAGS2 = { verbose: "-v", debug: "-d" };
|
|
2265
2277
|
var DcmodifyOptionsSchema = zod.z.object({
|
|
2266
2278
|
timeoutMs: zod.z.number().int().positive().optional(),
|
|
2267
2279
|
signal: zod.z.instanceof(AbortSignal).optional(),
|
|
@@ -2270,12 +2282,16 @@ var DcmodifyOptionsSchema = zod.z.object({
|
|
|
2270
2282
|
erasePrivateTags: zod.z.boolean().optional(),
|
|
2271
2283
|
noBackup: zod.z.boolean().optional(),
|
|
2272
2284
|
insertIfMissing: zod.z.boolean().optional(),
|
|
2273
|
-
ignoreMissingTags: zod.z.boolean().optional()
|
|
2285
|
+
ignoreMissingTags: zod.z.boolean().optional(),
|
|
2286
|
+
verbosity: zod.z.enum(["verbose", "debug"]).optional()
|
|
2274
2287
|
}).strict().refine((data) => data.modifications.length > 0 || data.erasures !== void 0 && data.erasures.length > 0 || data.erasePrivateTags === true, {
|
|
2275
2288
|
message: "At least one of modifications, erasures, or erasePrivateTags is required"
|
|
2276
2289
|
});
|
|
2277
2290
|
function buildArgs3(inputPath, options) {
|
|
2278
2291
|
const args = [];
|
|
2292
|
+
if (options.verbosity !== void 0) {
|
|
2293
|
+
args.push(VERBOSITY_FLAGS2[options.verbosity]);
|
|
2294
|
+
}
|
|
2279
2295
|
if (options.noBackup !== false) {
|
|
2280
2296
|
args.push("-nb");
|
|
2281
2297
|
}
|
|
@@ -2685,8 +2701,9 @@ var DEFAULT_MAX_POOL_SIZE = 10;
|
|
|
2685
2701
|
var DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
|
|
2686
2702
|
var CONNECTION_RETRY_INTERVAL_MS = 500;
|
|
2687
2703
|
var MAX_CONNECTION_RETRIES = 200;
|
|
2704
|
+
var MAX_OUTPUT_LINES_PER_ASSOCIATION = 500;
|
|
2688
2705
|
var DicomReceiverOptionsSchema = zod.z.object({
|
|
2689
|
-
port: zod.z.number().int().min(
|
|
2706
|
+
port: zod.z.number().int().min(0).max(65535),
|
|
2690
2707
|
storageDir: zod.z.string().min(1).refine(isSafePath, { message: "path traversal detected in storageDir" }),
|
|
2691
2708
|
aeTitle: zod.z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
|
|
2692
2709
|
minPoolSize: zod.z.number().int().min(1).max(100).optional(),
|
|
@@ -2694,6 +2711,9 @@ var DicomReceiverOptionsSchema = zod.z.object({
|
|
|
2694
2711
|
connectionTimeoutMs: zod.z.number().int().positive().optional(),
|
|
2695
2712
|
configFile: zod.z.string().min(1).refine(isSafePath, { message: "path traversal detected in configFile" }).optional(),
|
|
2696
2713
|
configProfile: zod.z.string().min(1).optional(),
|
|
2714
|
+
acseTimeout: zod.z.number().int().positive().optional(),
|
|
2715
|
+
dimseTimeout: zod.z.number().int().positive().optional(),
|
|
2716
|
+
maxPdu: zod.z.number().int().min(4096).max(131072).optional(),
|
|
2697
2717
|
signal: zod.z.instanceof(AbortSignal).optional()
|
|
2698
2718
|
}).strict().refine((data) => (data.minPoolSize ?? DEFAULT_MIN_POOL_SIZE) <= (data.maxPoolSize ?? DEFAULT_MAX_POOL_SIZE), {
|
|
2699
2719
|
message: "minPoolSize must be <= maxPoolSize"
|
|
@@ -2822,6 +2842,57 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
2822
2842
|
onAssociationComplete(listener) {
|
|
2823
2843
|
return this.on("ASSOCIATION_COMPLETE", listener);
|
|
2824
2844
|
}
|
|
2845
|
+
/**
|
|
2846
|
+
* Registers a listener for new associations (bubbled from workers).
|
|
2847
|
+
*
|
|
2848
|
+
* @param listener - Callback receiving association received data
|
|
2849
|
+
* @returns this for chaining
|
|
2850
|
+
*/
|
|
2851
|
+
onAssociationReceived(listener) {
|
|
2852
|
+
return this.on("ASSOCIATION_RECEIVED", listener);
|
|
2853
|
+
}
|
|
2854
|
+
/**
|
|
2855
|
+
* Registers a listener for C-STORE requests (bubbled from workers).
|
|
2856
|
+
*
|
|
2857
|
+
* @param listener - Callback receiving C-STORE request data
|
|
2858
|
+
* @returns this for chaining
|
|
2859
|
+
*/
|
|
2860
|
+
onCStoreRequest(listener) {
|
|
2861
|
+
return this.on("C_STORE_REQUEST", listener);
|
|
2862
|
+
}
|
|
2863
|
+
/**
|
|
2864
|
+
* Registers a listener for C-ECHO requests (bubbled from workers).
|
|
2865
|
+
*
|
|
2866
|
+
* @param listener - Callback receiving echo request data
|
|
2867
|
+
* @returns this for chaining
|
|
2868
|
+
*/
|
|
2869
|
+
onEchoRequest(listener) {
|
|
2870
|
+
return this.on("ECHO_REQUEST", listener);
|
|
2871
|
+
}
|
|
2872
|
+
/**
|
|
2873
|
+
* Registers a listener for refused associations (bubbled from workers).
|
|
2874
|
+
*
|
|
2875
|
+
* @param listener - Callback receiving refusing association data
|
|
2876
|
+
* @returns this for chaining
|
|
2877
|
+
*/
|
|
2878
|
+
onRefusingAssociation(listener) {
|
|
2879
|
+
return this.on("REFUSING_ASSOCIATION", listener);
|
|
2880
|
+
}
|
|
2881
|
+
/**
|
|
2882
|
+
* Routes an external socket to an idle worker.
|
|
2883
|
+
*
|
|
2884
|
+
* The pool must be started via `start()` before calling this method.
|
|
2885
|
+
* Use this when managing your own TCP listener (e.g., protocol router).
|
|
2886
|
+
*
|
|
2887
|
+
* @param socket - An incoming net.Socket to route to a worker
|
|
2888
|
+
*/
|
|
2889
|
+
handleSocket(socket) {
|
|
2890
|
+
if (!this.started) {
|
|
2891
|
+
socket.destroy(new Error("DicomReceiver: not started"));
|
|
2892
|
+
return;
|
|
2893
|
+
}
|
|
2894
|
+
void this.handleConnection(socket);
|
|
2895
|
+
}
|
|
2825
2896
|
/** Current pool status. */
|
|
2826
2897
|
get poolStatus() {
|
|
2827
2898
|
let idle = 0;
|
|
@@ -2835,8 +2906,11 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
2835
2906
|
// -----------------------------------------------------------------------
|
|
2836
2907
|
// TCP proxy
|
|
2837
2908
|
// -----------------------------------------------------------------------
|
|
2838
|
-
/** Starts the TCP proxy on the configured port. */
|
|
2909
|
+
/** Starts the TCP proxy on the configured port. Skips if port is 0. */
|
|
2839
2910
|
startTcpProxy() {
|
|
2911
|
+
if (this.options.port === 0) {
|
|
2912
|
+
return Promise.resolve(ok(void 0));
|
|
2913
|
+
}
|
|
2840
2914
|
return new Promise((resolve) => {
|
|
2841
2915
|
this.tcpServer = net__namespace.createServer((socket) => {
|
|
2842
2916
|
void this.handleConnection(socket);
|
|
@@ -2889,6 +2963,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
2889
2963
|
worker.associationDir = associationDir;
|
|
2890
2964
|
worker.files = [];
|
|
2891
2965
|
worker.fileSizes = [];
|
|
2966
|
+
worker.outputLines = [];
|
|
2892
2967
|
worker.startAt = Date.now();
|
|
2893
2968
|
this.pipeConnection(worker, remoteSocket);
|
|
2894
2969
|
void this.replenishPool();
|
|
@@ -2947,6 +3022,19 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
2947
3022
|
}
|
|
2948
3023
|
return ok(void 0);
|
|
2949
3024
|
}
|
|
3025
|
+
/** Creates a Dcmrecv instance with the pool's shared options. */
|
|
3026
|
+
createDcmrecv(port, tempDir) {
|
|
3027
|
+
return Dcmrecv.create({
|
|
3028
|
+
port,
|
|
3029
|
+
aeTitle: this.options.aeTitle ?? "DCMRECV",
|
|
3030
|
+
outputDirectory: tempDir,
|
|
3031
|
+
configFile: this.options.configFile,
|
|
3032
|
+
configProfile: this.options.configProfile,
|
|
3033
|
+
acseTimeout: this.options.acseTimeout,
|
|
3034
|
+
dimseTimeout: this.options.dimseTimeout,
|
|
3035
|
+
maxPdu: this.options.maxPdu
|
|
3036
|
+
});
|
|
3037
|
+
}
|
|
2950
3038
|
/** Spawns a single Dcmrecv worker with an ephemeral port. */
|
|
2951
3039
|
async spawnWorker() {
|
|
2952
3040
|
const portResult = await allocatePort();
|
|
@@ -2955,13 +3043,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
2955
3043
|
const tempDir = path__namespace.join(os__namespace.tmpdir(), `dcmrecv-pool-${String(port)}-${String(Date.now())}`);
|
|
2956
3044
|
const mkdirResult = await ensureDirectory(tempDir);
|
|
2957
3045
|
if (!mkdirResult.ok) return mkdirResult;
|
|
2958
|
-
const createResult =
|
|
2959
|
-
port,
|
|
2960
|
-
aeTitle: this.options.aeTitle ?? "DCMRECV",
|
|
2961
|
-
outputDirectory: tempDir,
|
|
2962
|
-
configFile: this.options.configFile,
|
|
2963
|
-
configProfile: this.options.configProfile
|
|
2964
|
-
});
|
|
3046
|
+
const createResult = this.createDcmrecv(port, tempDir);
|
|
2965
3047
|
if (!createResult.ok) return createResult;
|
|
2966
3048
|
const dcmrecv = createResult.value;
|
|
2967
3049
|
const worker = {
|
|
@@ -2973,6 +3055,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
2973
3055
|
associationDir: void 0,
|
|
2974
3056
|
files: [],
|
|
2975
3057
|
fileSizes: [],
|
|
3058
|
+
outputLines: [],
|
|
2976
3059
|
startAt: void 0,
|
|
2977
3060
|
remoteSocket: void 0,
|
|
2978
3061
|
workerSocket: void 0
|
|
@@ -3034,10 +3117,15 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
3034
3117
|
// -----------------------------------------------------------------------
|
|
3035
3118
|
// Worker event wiring
|
|
3036
3119
|
// -----------------------------------------------------------------------
|
|
3037
|
-
/** Wires
|
|
3120
|
+
/** Wires all events on a worker: file handling, association lifecycle, output capture. */
|
|
3038
3121
|
wireWorkerEvents(worker) {
|
|
3039
3122
|
this.wireFileReceived(worker);
|
|
3040
3123
|
this.wireAssociationComplete(worker);
|
|
3124
|
+
this.wireAssociationReceived(worker);
|
|
3125
|
+
this.wireCStoreRequest(worker);
|
|
3126
|
+
this.wireEchoRequest(worker);
|
|
3127
|
+
this.wireRefusingAssociation(worker);
|
|
3128
|
+
this.wireOutputCapture(worker);
|
|
3041
3129
|
}
|
|
3042
3130
|
/** Wires FILE_RECEIVED from dcmrecv worker to handleFileReceived. */
|
|
3043
3131
|
wireFileReceived(worker) {
|
|
@@ -3085,6 +3173,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
3085
3173
|
const assocId = worker.associationId ?? data.associationId;
|
|
3086
3174
|
const assocDir = worker.associationDir ?? "";
|
|
3087
3175
|
const files = [...worker.files];
|
|
3176
|
+
const output = [...worker.outputLines];
|
|
3088
3177
|
const endAt = Date.now();
|
|
3089
3178
|
const startAt = worker.startAt ?? endAt;
|
|
3090
3179
|
const totalBytes = sumArray(worker.fileSizes);
|
|
@@ -3102,19 +3191,65 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
3102
3191
|
totalBytes,
|
|
3103
3192
|
bytesPerSecond,
|
|
3104
3193
|
startAt,
|
|
3105
|
-
endAt
|
|
3194
|
+
endAt,
|
|
3195
|
+
output
|
|
3106
3196
|
});
|
|
3107
3197
|
worker.state = "idle";
|
|
3108
3198
|
worker.associationId = void 0;
|
|
3109
3199
|
worker.associationDir = void 0;
|
|
3110
3200
|
worker.files = [];
|
|
3111
3201
|
worker.fileSizes = [];
|
|
3202
|
+
worker.outputLines = [];
|
|
3112
3203
|
worker.startAt = void 0;
|
|
3113
3204
|
worker.remoteSocket = void 0;
|
|
3114
3205
|
worker.workerSocket = void 0;
|
|
3115
3206
|
void this.scaleDown();
|
|
3116
3207
|
});
|
|
3117
3208
|
}
|
|
3209
|
+
/** Bubbles ASSOCIATION_RECEIVED from dcmrecv worker. */
|
|
3210
|
+
wireAssociationReceived(worker) {
|
|
3211
|
+
worker.dcmrecv.onEvent("ASSOCIATION_RECEIVED", (data) => {
|
|
3212
|
+
this.emit("ASSOCIATION_RECEIVED", {
|
|
3213
|
+
associationId: worker.associationId ?? "",
|
|
3214
|
+
callingAE: data.callingAE,
|
|
3215
|
+
calledAE: data.calledAE,
|
|
3216
|
+
source: data.source
|
|
3217
|
+
});
|
|
3218
|
+
});
|
|
3219
|
+
}
|
|
3220
|
+
/** Bubbles C_STORE_REQUEST from dcmrecv worker. */
|
|
3221
|
+
wireCStoreRequest(worker) {
|
|
3222
|
+
worker.dcmrecv.onEvent("C_STORE_REQUEST", (data) => {
|
|
3223
|
+
this.emit("C_STORE_REQUEST", {
|
|
3224
|
+
associationId: worker.associationId ?? "",
|
|
3225
|
+
raw: data.raw
|
|
3226
|
+
});
|
|
3227
|
+
});
|
|
3228
|
+
}
|
|
3229
|
+
/** Bubbles ECHO_REQUEST from dcmrecv worker. */
|
|
3230
|
+
wireEchoRequest(worker) {
|
|
3231
|
+
worker.dcmrecv.onEvent("ECHO_REQUEST", () => {
|
|
3232
|
+
this.emit("ECHO_REQUEST", {
|
|
3233
|
+
associationId: worker.associationId ?? ""
|
|
3234
|
+
});
|
|
3235
|
+
});
|
|
3236
|
+
}
|
|
3237
|
+
/** Bubbles REFUSING_ASSOCIATION from dcmrecv worker. */
|
|
3238
|
+
wireRefusingAssociation(worker) {
|
|
3239
|
+
worker.dcmrecv.onEvent("REFUSING_ASSOCIATION", (data) => {
|
|
3240
|
+
this.emit("REFUSING_ASSOCIATION", {
|
|
3241
|
+
reason: data.reason
|
|
3242
|
+
});
|
|
3243
|
+
});
|
|
3244
|
+
}
|
|
3245
|
+
/** Captures worker output lines during busy associations. */
|
|
3246
|
+
wireOutputCapture(worker) {
|
|
3247
|
+
worker.dcmrecv.on("line", ({ text }) => {
|
|
3248
|
+
if (worker.state === "busy" && worker.outputLines.length < MAX_OUTPUT_LINES_PER_ASSOCIATION) {
|
|
3249
|
+
worker.outputLines.push(text);
|
|
3250
|
+
}
|
|
3251
|
+
});
|
|
3252
|
+
}
|
|
3118
3253
|
// -----------------------------------------------------------------------
|
|
3119
3254
|
// Abort signal
|
|
3120
3255
|
// -----------------------------------------------------------------------
|