@j-o-r/sh 1.1.15 → 1.1.17
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/lib/AsyncTracker.js +259 -0
- package/lib/SH.js +7 -11
- package/lib/SHExecute.js +0 -1
- package/lib/Test.js +1 -1
- package/package.json +2 -3
- package/types/AsyncTracker.d.ts +66 -0
- package/types/SH.d.ts +2 -1
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
|
|
2
|
+
// Copyright 2023 J.H. Duin
|
|
3
|
+
//
|
|
4
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
// you may not use this file except in compliance with the License.
|
|
6
|
+
// You may obtain a copy of the License at
|
|
7
|
+
//
|
|
8
|
+
// https://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
//
|
|
10
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
// See the License for the specific language governing permissions and
|
|
14
|
+
// limitations under the License.
|
|
15
|
+
import { writeFileSync } from 'node:fs';
|
|
16
|
+
import { format } from 'node:util';
|
|
17
|
+
import async_hooks from 'async_hooks';
|
|
18
|
+
|
|
19
|
+
const DEBUG = false;
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {Object} AsyncHookItem
|
|
22
|
+
* Represents an item in the async hooks tracking.
|
|
23
|
+
*
|
|
24
|
+
* @property {number} key - The unique identifier for the async operation.
|
|
25
|
+
* @property {string} type - The type of the async operation, e.g., 'PROMISE'.
|
|
26
|
+
* @property {number} triggerAsyncId - The async ID of the resource that triggered this operation.
|
|
27
|
+
* @property {string} stack - The call stack trace when the async operation was initialized.
|
|
28
|
+
* @property {SystemTypes} resource - The Promise object associated with this operation, including its state and async IDs.-
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {'PROMISE'|'TIMEOUT'|'PROCESSNEXTTICK'|'TICKOBJECT' | 'SCRIPT' |'QUERYWRAP' | 'FILEHANDLE' | 'HTTP2SESSION' | 'HTTP2STREAM' | 'ZLIB' | 'UDPSENDWRAP' | 'WRITEWRAP' | 'SHUTDOWNWRAP' | 'PROMISEEXECUTOR'|'TCPCONNECTWRAP'| 'GETADDRINFOREQWRAP' | 'GETNAMEINFOREQWRAP' |'IMMEDIATE'|'TCPWRAP'|'TCPSERVERWRAP'|'UDPWRAP'|'FSREQCALLBACK'|'HTTPPARSER'|'PIPEWRAP'|'PIPECONNECTWRAP'|'STREAMWRAP'|'TTYWRAP'|'PROCESS'|'SIGNALWRAP'|'TIMERWRAP'} SystemTypes
|
|
32
|
+
/**
|
|
33
|
+
* @param {any[]} args
|
|
34
|
+
*/
|
|
35
|
+
function debug(...args) {
|
|
36
|
+
if (!DEBUG) return;
|
|
37
|
+
// Use a function like this one when debugging inside an AsyncHook callback
|
|
38
|
+
writeFileSync('debug.out', `${format(...args)}\n`, { flag: 'a' });
|
|
39
|
+
}
|
|
40
|
+
const TYPES = {
|
|
41
|
+
TIMERWRAP: 'setTimeout(), setInterval()',
|
|
42
|
+
PROMISE: 'Promise',
|
|
43
|
+
IMMEDIATE: 'setImmediate()',
|
|
44
|
+
PROCESSNEXTTICK: 'process.nextTick()',
|
|
45
|
+
TICKOBJECT: 'Used internally by process.nextTick()',
|
|
46
|
+
SCRIPT: 'vm.Script.runInThisContext()',
|
|
47
|
+
PROMISEEXECUTOR: 'Used internally by Promise',
|
|
48
|
+
TIMEOUT: 'setTimeout()',
|
|
49
|
+
PIPECONNECTWRAP: 'Used by pipes',
|
|
50
|
+
PIPEWRAP: 'Used by pipes',
|
|
51
|
+
TCPCONNECTWRAP: 'Used by TCP sockets',
|
|
52
|
+
TCPWRAP: 'Used by TCP sockets',
|
|
53
|
+
GETADDRINFOREQWRAP: 'Used by DNS queries',
|
|
54
|
+
GETNAMEINFOREQWRAP: 'Used by DNS queries',
|
|
55
|
+
QUERYWRAP: 'Used by DNS queries',
|
|
56
|
+
FSREQCALLBACK: 'Used by FS operations',
|
|
57
|
+
FILEHANDLE: 'Used by file handles',
|
|
58
|
+
SIGNALWRAP: 'Used by signal handlers',
|
|
59
|
+
HTTPPARSER: 'Used by HTTP parsing',
|
|
60
|
+
HTTP2SESSION: 'Used by HTTP/2 sessions',
|
|
61
|
+
HTTP2STREAM: 'Used by HTTP/2 streams',
|
|
62
|
+
ZLIB: 'Used by Zlib',
|
|
63
|
+
TTYWRAP: 'Used by TTY handles',
|
|
64
|
+
UDPSENDWRAP: 'Used by UDP sockets',
|
|
65
|
+
UDPWRAP: 'Used by UDP sockets',
|
|
66
|
+
WRITEWRAP: 'Used by process.stdout and process.stderr',
|
|
67
|
+
SHUTDOWNWRAP: 'Used by socket.end()',
|
|
68
|
+
};
|
|
69
|
+
/*
|
|
70
|
+
--- resourceTypes ---
|
|
71
|
+
'Promise',
|
|
72
|
+
'Timeout',
|
|
73
|
+
'Immediate',
|
|
74
|
+
'TCPWrap',
|
|
75
|
+
'TCPSERVERWRAP',
|
|
76
|
+
'UDPWrap',
|
|
77
|
+
'FSReqCallback',
|
|
78
|
+
'HTTPParser',
|
|
79
|
+
'PipeWrap',
|
|
80
|
+
'PipeConnectWrap',
|
|
81
|
+
'StreamWrap',
|
|
82
|
+
'TtyWrap',
|
|
83
|
+
'Process',
|
|
84
|
+
'SignalWrap',
|
|
85
|
+
'TimerWrap'
|
|
86
|
+
*/
|
|
87
|
+
/**
|
|
88
|
+
* @param {SystemTypes} type
|
|
89
|
+
*/
|
|
90
|
+
const getTypeDescription = (type) => {
|
|
91
|
+
if (TYPES[type]) return TYPES[type];
|
|
92
|
+
const err = `Unknown type: ${type}`;
|
|
93
|
+
throw new Error(err);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
class AsyncTracker {
|
|
97
|
+
#enabled = false;
|
|
98
|
+
#filter = '';
|
|
99
|
+
#storage = new Map();
|
|
100
|
+
/** @type {import('async_hooks').AsyncHook} */
|
|
101
|
+
#hook;
|
|
102
|
+
constructor() {
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 'arm' the registration of async methods
|
|
106
|
+
*/
|
|
107
|
+
#arm() {
|
|
108
|
+
if (this.#hook) {
|
|
109
|
+
this.#hook.disable();
|
|
110
|
+
}
|
|
111
|
+
this.#storage.clear();
|
|
112
|
+
const storage = this.#storage;
|
|
113
|
+
const filter = this.#filter
|
|
114
|
+
// CreateHook triggers a Promise itself
|
|
115
|
+
this.#hook = async_hooks.createHook({
|
|
116
|
+
/**
|
|
117
|
+
* @param {number} asyncId
|
|
118
|
+
* @param {string} type
|
|
119
|
+
* @param {number} triggerAsyncId
|
|
120
|
+
* @param {object} resource
|
|
121
|
+
*/
|
|
122
|
+
init(asyncId, type, triggerAsyncId, resource) {
|
|
123
|
+
type = type.toUpperCase();
|
|
124
|
+
if (filter === '' || filter === type) {
|
|
125
|
+
const stackString = new Error().stack;
|
|
126
|
+
const stackLines = stackString.split("\n");
|
|
127
|
+
const stack = stackLines.slice(6, 20).join("\n").trim();
|
|
128
|
+
// Only add a type where we can trace back to a file
|
|
129
|
+
const hasFile = stack.includes('file:///');
|
|
130
|
+
if (hasFile) {
|
|
131
|
+
debug({ action: 'set', asyncId, type, hasFile });
|
|
132
|
+
// debug({ action: 'set', asyncId, type})
|
|
133
|
+
storage.set(asyncId, { type, triggerAsyncId, stack, resource });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
/**
|
|
138
|
+
* @param {number} asyncId
|
|
139
|
+
*/
|
|
140
|
+
after(asyncId) {
|
|
141
|
+
// // after() is called just after the resource's callback has finished.
|
|
142
|
+
if (storage.has(asyncId)) {
|
|
143
|
+
debug({ type: 'after', asyncId })
|
|
144
|
+
storage.delete(asyncId);
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
/**
|
|
148
|
+
* @param {number} asyncId
|
|
149
|
+
*/
|
|
150
|
+
destroy(asyncId) {
|
|
151
|
+
// `destroy` is called by the garbage collector once resolved/runned
|
|
152
|
+
if (storage.has(asyncId)) {
|
|
153
|
+
debug({ type: 'destroy', asyncId })
|
|
154
|
+
storage.delete(asyncId);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @param {number} asyncId
|
|
160
|
+
*/
|
|
161
|
+
promiseResolve(asyncId) {
|
|
162
|
+
if (storage.has(asyncId)) {
|
|
163
|
+
debug({ type: 'resolve', asyncId })
|
|
164
|
+
storage.delete(asyncId);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Enables tracking of asynchronous methods. Optionally, only methods of a specified type can be tracked.
|
|
172
|
+
* @param {SystemTypes} [type] - optional only register async methods for a specific type
|
|
173
|
+
*/
|
|
174
|
+
enable(type) {
|
|
175
|
+
if (type && !TYPES[type.toUpperCase()]) {
|
|
176
|
+
const err = `Unknown type: ${type}`;
|
|
177
|
+
throw new Error(err);
|
|
178
|
+
}
|
|
179
|
+
this.#filter = type ? type.toUpperCase() : '';
|
|
180
|
+
this.#arm();
|
|
181
|
+
this.#enabled = true;
|
|
182
|
+
this.#hook.enable()
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Disables the tracking of asynchronous methods.
|
|
186
|
+
*/
|
|
187
|
+
disable() {
|
|
188
|
+
if (this.#hook) this.#hook.disable();
|
|
189
|
+
this.#enabled = false;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Clears all tracked asynchronous methods.
|
|
193
|
+
*/
|
|
194
|
+
reset() {
|
|
195
|
+
this.#storage.clear();
|
|
196
|
+
}
|
|
197
|
+
/*
|
|
198
|
+
* Prints an overview of active asynchronous calls.
|
|
199
|
+
* @param {boolean} [verbose] - default false, print an overview of active async calls
|
|
200
|
+
* @returns {number} Number of active, unresolved async calls
|
|
201
|
+
*/
|
|
202
|
+
report(verbose = false) {
|
|
203
|
+
const size = this.#storage.size;
|
|
204
|
+
if (verbose) {
|
|
205
|
+
let i = 0;
|
|
206
|
+
if (size > 0) process.stdout.write('\n');
|
|
207
|
+
this.#storage.forEach((value, key) => {
|
|
208
|
+
i++;
|
|
209
|
+
process.stdout.write(`Async resource of type ${value.type} with ID ${key}:\n`);
|
|
210
|
+
process.stdout.write(`Stack: ${value.stack}\n`);
|
|
211
|
+
process.stdout.write(`Resource: ${value.resource}\n`);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return size;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Returns an array of unresolved asynchronous methods, optionally filtered by type
|
|
218
|
+
* @param {SystemTypes} [type] - The type of async methods to filter by
|
|
219
|
+
* @returns {AsyncHookItem[]}
|
|
220
|
+
*/
|
|
221
|
+
getUnresolved(type) {
|
|
222
|
+
if (this.#enabled) {
|
|
223
|
+
this.#hook.disable();
|
|
224
|
+
}
|
|
225
|
+
// @ts-ignore
|
|
226
|
+
if (type) type = type.toUpperCase();
|
|
227
|
+
let items = Array.from(this.#storage, ([key, value]) => ({ key, ...value }));
|
|
228
|
+
if (type) {
|
|
229
|
+
items = items.filter(item => item.type === type);
|
|
230
|
+
}
|
|
231
|
+
if (this.#enabled) {
|
|
232
|
+
this.#hook.enable();
|
|
233
|
+
}
|
|
234
|
+
return items;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Returns a description of the specified asynchronous method type
|
|
238
|
+
* @param {SystemTypes} type - The type of asynchronous method.
|
|
239
|
+
* @returns {string}
|
|
240
|
+
*/
|
|
241
|
+
getTypeDescription(type) {
|
|
242
|
+
// @ts-ignore
|
|
243
|
+
type = type.toUpperCase();
|
|
244
|
+
return getTypeDescription(type)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Adds or overwrites a custom type for asynchronous methods.
|
|
249
|
+
* @param {string} type - The type of asynchronous method.
|
|
250
|
+
* @param {string} description - the description
|
|
251
|
+
* @returns {void}
|
|
252
|
+
*/
|
|
253
|
+
addCustomType(type, description) {
|
|
254
|
+
type = type.toUpperCase();
|
|
255
|
+
TYPES[type] = description;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export default AsyncTracker;
|
package/lib/SH.js
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
import assert from 'node:assert';
|
|
30
30
|
import SHDispatch from './SHDispatch.js';
|
|
31
31
|
import Test from './Test.js'
|
|
32
|
+
import AsyncTracker from './AsyncTracker.js'
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* @typedef {Object.<string, string>} ArgsObject
|
|
@@ -98,9 +99,12 @@ const SH = new Proxy(function(pieces, ...args) {
|
|
|
98
99
|
let cmd = pieces[0], i = 0;
|
|
99
100
|
while (i < args.length) {
|
|
100
101
|
let s;
|
|
102
|
+
|
|
101
103
|
if (Array.isArray(args[i])) {
|
|
102
104
|
s = args[i].map(/** @param {string} x */(x) => {
|
|
103
|
-
|
|
105
|
+
// Trim every element
|
|
106
|
+
let str = String(x).trim();
|
|
107
|
+
str = str.replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t');
|
|
104
108
|
// If the string contains special characters, wrap in single quotes
|
|
105
109
|
if (str.match(/[ "'$`(){}[\]]/)) {
|
|
106
110
|
// Escape single quotes within the string
|
|
@@ -109,15 +113,6 @@ const SH = new Proxy(function(pieces, ...args) {
|
|
|
109
113
|
return str;
|
|
110
114
|
}).join(' ');
|
|
111
115
|
} else {
|
|
112
|
-
// let str = String(args[i]);
|
|
113
|
-
// if (str.match(/[ "'$`(){}[\]]/)) {
|
|
114
|
-
// s = `'${str.replace(/'/g, `'\\''`)}'`;
|
|
115
|
-
// } else {
|
|
116
|
-
// s = str;
|
|
117
|
-
// }
|
|
118
|
-
// Leave string UNALTERED
|
|
119
|
-
// Should be escaped in the code and is the responsibility of the developer
|
|
120
|
-
// SH`do something "${filename}"`;
|
|
121
116
|
s = String(args[i]);
|
|
122
117
|
}
|
|
123
118
|
cmd += s + pieces[++i];
|
|
@@ -311,5 +306,6 @@ export {
|
|
|
311
306
|
parseArgs,
|
|
312
307
|
jsType,
|
|
313
308
|
Test,
|
|
314
|
-
assert
|
|
309
|
+
assert,
|
|
310
|
+
AsyncTracker
|
|
315
311
|
}
|
package/lib/SHExecute.js
CHANGED
package/lib/Test.js
CHANGED
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@j-o-r/sh",
|
|
3
3
|
"author": "Jorrit Duin <j-o-r@duin.work>",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "1.1.
|
|
5
|
+
"version": "1.1.17",
|
|
6
6
|
"description": "Execute shell commands on Linux-based systems from javascript",
|
|
7
7
|
"main": "lib/SH.js",
|
|
8
8
|
"types": "types/SH.d.ts",
|
|
@@ -21,8 +21,7 @@
|
|
|
21
21
|
"url": "https://codeberg.org/duin/sh"
|
|
22
22
|
},
|
|
23
23
|
"license": "Apache License, Version 2.0",
|
|
24
|
-
"
|
|
25
|
-
"@j-o-r/asynctracker": "^0.0.8",
|
|
24
|
+
"devDependencies": {
|
|
26
25
|
"@types/node": "^22.10.10"
|
|
27
26
|
},
|
|
28
27
|
"bugs": {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export default AsyncTracker;
|
|
2
|
+
/**
|
|
3
|
+
* Represents an item in the async hooks tracking.
|
|
4
|
+
*/
|
|
5
|
+
export type AsyncHookItem = {
|
|
6
|
+
/**
|
|
7
|
+
* - The unique identifier for the async operation.
|
|
8
|
+
*/
|
|
9
|
+
key: number;
|
|
10
|
+
/**
|
|
11
|
+
* - The type of the async operation, e.g., 'PROMISE'.
|
|
12
|
+
*/
|
|
13
|
+
type: string;
|
|
14
|
+
/**
|
|
15
|
+
* - The async ID of the resource that triggered this operation.
|
|
16
|
+
*/
|
|
17
|
+
triggerAsyncId: number;
|
|
18
|
+
/**
|
|
19
|
+
* - The call stack trace when the async operation was initialized.
|
|
20
|
+
*/
|
|
21
|
+
stack: string;
|
|
22
|
+
/**
|
|
23
|
+
* - The Promise object associated with this operation, including its state and async IDs.-
|
|
24
|
+
*/
|
|
25
|
+
resource: SystemTypes;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* /**
|
|
29
|
+
*/
|
|
30
|
+
export type SystemTypes = "PROMISE" | "TIMEOUT" | "PROCESSNEXTTICK" | "TICKOBJECT" | "SCRIPT" | "QUERYWRAP" | "FILEHANDLE" | "HTTP2SESSION" | "HTTP2STREAM" | "ZLIB" | "UDPSENDWRAP" | "WRITEWRAP" | "SHUTDOWNWRAP" | "PROMISEEXECUTOR" | "TCPCONNECTWRAP" | "GETADDRINFOREQWRAP" | "GETNAMEINFOREQWRAP" | "IMMEDIATE" | "TCPWRAP" | "TCPSERVERWRAP" | "UDPWRAP" | "FSREQCALLBACK" | "HTTPPARSER" | "PIPEWRAP" | "PIPECONNECTWRAP" | "STREAMWRAP" | "TTYWRAP" | "PROCESS" | "SIGNALWRAP" | "TIMERWRAP";
|
|
31
|
+
declare class AsyncTracker {
|
|
32
|
+
/**
|
|
33
|
+
* Enables tracking of asynchronous methods. Optionally, only methods of a specified type can be tracked.
|
|
34
|
+
* @param {SystemTypes} [type] - optional only register async methods for a specific type
|
|
35
|
+
*/
|
|
36
|
+
enable(type?: SystemTypes): void;
|
|
37
|
+
/**
|
|
38
|
+
* Disables the tracking of asynchronous methods.
|
|
39
|
+
*/
|
|
40
|
+
disable(): void;
|
|
41
|
+
/**
|
|
42
|
+
* Clears all tracked asynchronous methods.
|
|
43
|
+
*/
|
|
44
|
+
reset(): void;
|
|
45
|
+
report(verbose?: boolean): number;
|
|
46
|
+
/**
|
|
47
|
+
* Returns an array of unresolved asynchronous methods, optionally filtered by type
|
|
48
|
+
* @param {SystemTypes} [type] - The type of async methods to filter by
|
|
49
|
+
* @returns {AsyncHookItem[]}
|
|
50
|
+
*/
|
|
51
|
+
getUnresolved(type?: SystemTypes): AsyncHookItem[];
|
|
52
|
+
/**
|
|
53
|
+
* Returns a description of the specified asynchronous method type
|
|
54
|
+
* @param {SystemTypes} type - The type of asynchronous method.
|
|
55
|
+
* @returns {string}
|
|
56
|
+
*/
|
|
57
|
+
getTypeDescription(type: SystemTypes): string;
|
|
58
|
+
/**
|
|
59
|
+
* Adds or overwrites a custom type for asynchronous methods.
|
|
60
|
+
* @param {string} type - The type of asynchronous method.
|
|
61
|
+
* @param {string} description - the description
|
|
62
|
+
* @returns {void}
|
|
63
|
+
*/
|
|
64
|
+
addCustomType(type: string, description: string): void;
|
|
65
|
+
#private;
|
|
66
|
+
}
|
package/types/SH.d.ts
CHANGED
|
@@ -128,5 +128,6 @@ export function parseArgs(args?: string[]): ArgsObject;
|
|
|
128
128
|
export function jsType(fn: any): string;
|
|
129
129
|
import Test from './Test.js';
|
|
130
130
|
import assert from 'node:assert';
|
|
131
|
+
import AsyncTracker from './AsyncTracker.js';
|
|
131
132
|
import SHDispatch from './SHDispatch.js';
|
|
132
|
-
export { Test, assert };
|
|
133
|
+
export { Test, assert, AsyncTracker };
|