@j-o-r/sh 1.1.15 → 1.1.16

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.
@@ -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
@@ -311,5 +312,6 @@ export {
311
312
  parseArgs,
312
313
  jsType,
313
314
  Test,
314
- assert
315
+ assert,
316
+ AsyncTracker
315
317
  }
package/lib/Test.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { nextTick } from 'process';
2
2
  import { jsType } from './SH.js'
3
- import AsyncTracker from '@j-o-r/asynctracker';
3
+ import AsyncTracker from './AsyncTracker.js';
4
4
  /**
5
5
  * Valid jsTypes for test methods
6
6
  */
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.15",
5
+ "version": "1.1.16",
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
- "dependencies": {
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 };