@nomicfoundation/hardhat-utils 3.0.0-next.9 → 3.0.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 +7 -5
- package/README.md +3 -3
- package/dist/src/ci.d.ts +4 -0
- package/dist/src/ci.d.ts.map +1 -1
- package/dist/src/ci.js +4 -0
- package/dist/src/ci.js.map +1 -1
- package/dist/src/crypto.d.ts +7 -0
- package/dist/src/crypto.d.ts.map +1 -1
- package/dist/src/crypto.js +10 -0
- package/dist/src/crypto.js.map +1 -1
- package/dist/src/debug.d.ts.map +1 -1
- package/dist/src/env.d.ts +19 -0
- package/dist/src/env.d.ts.map +1 -0
- package/dist/src/env.js +29 -0
- package/dist/src/env.js.map +1 -0
- package/dist/src/error.d.ts +2 -2
- package/dist/src/error.js +2 -2
- package/dist/src/fs.d.ts +7 -3
- package/dist/src/fs.d.ts.map +1 -1
- package/dist/src/fs.js +28 -5
- package/dist/src/fs.js.map +1 -1
- package/dist/src/internal/bytecode.d.ts +1 -1
- package/dist/src/internal/bytecode.d.ts.map +1 -1
- package/dist/src/lang.d.ts +7 -0
- package/dist/src/lang.d.ts.map +1 -1
- package/dist/src/lang.js +30 -0
- package/dist/src/lang.js.map +1 -1
- package/dist/src/package.d.ts +12 -0
- package/dist/src/package.d.ts.map +1 -1
- package/dist/src/package.js.map +1 -1
- package/dist/src/panic-errors.d.ts +2 -0
- package/dist/src/panic-errors.d.ts.map +1 -0
- package/dist/src/panic-errors.js +32 -0
- package/dist/src/panic-errors.js.map +1 -0
- package/dist/src/path.d.ts +2 -2
- package/dist/src/path.d.ts.map +1 -1
- package/dist/src/path.js +12 -3
- package/dist/src/path.js.map +1 -1
- package/dist/src/request.d.ts +26 -6
- package/dist/src/request.d.ts.map +1 -1
- package/dist/src/request.js +37 -4
- package/dist/src/request.js.map +1 -1
- package/dist/src/stream.d.ts +1 -1
- package/dist/src/stream.js +1 -1
- package/dist/src/subprocess.js +1 -1
- package/dist/src/subprocess.js.map +1 -1
- package/dist/src/synchronization.d.ts +17 -0
- package/dist/src/synchronization.d.ts.map +1 -1
- package/dist/src/synchronization.js +77 -1
- package/dist/src/synchronization.js.map +1 -1
- package/package.json +6 -4
- package/src/ci.ts +5 -0
- package/src/crypto.ts +11 -0
- package/src/env.ts +33 -0
- package/src/error.ts +2 -2
- package/src/fs.ts +43 -8
- package/src/internal/bytecode.ts +1 -1
- package/src/lang.ts +36 -0
- package/src/package.ts +26 -0
- package/src/panic-errors.ts +35 -0
- package/src/path.ts +16 -6
- package/src/request.ts +55 -8
- package/src/stream.ts +1 -1
- package/src/subprocess.ts +1 -1
- package/src/synchronization.ts +90 -1
package/src/fs.ts
CHANGED
|
@@ -56,8 +56,8 @@ export async function getRealPath(absolutePath: string): Promise<string> {
|
|
|
56
56
|
*/
|
|
57
57
|
export async function getAllFilesMatching(
|
|
58
58
|
dirFrom: string,
|
|
59
|
-
matches?: (absolutePathToFile: string) => boolean,
|
|
60
|
-
directoryFilter?: (absolutePathToDir: string) => boolean,
|
|
59
|
+
matches?: (absolutePathToFile: string) => Promise<boolean> | boolean,
|
|
60
|
+
directoryFilter?: (absolutePathToDir: string) => Promise<boolean> | boolean,
|
|
61
61
|
): Promise<string[]> {
|
|
62
62
|
const dirContent = await readdirOrEmpty(dirFrom);
|
|
63
63
|
|
|
@@ -67,7 +67,7 @@ export async function getAllFilesMatching(
|
|
|
67
67
|
if (await isDirectory(absolutePathToFile)) {
|
|
68
68
|
if (
|
|
69
69
|
directoryFilter === undefined ||
|
|
70
|
-
directoryFilter(absolutePathToFile)
|
|
70
|
+
(await directoryFilter(absolutePathToFile))
|
|
71
71
|
) {
|
|
72
72
|
return getAllFilesMatching(
|
|
73
73
|
absolutePathToFile,
|
|
@@ -77,7 +77,7 @@ export async function getAllFilesMatching(
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
return [];
|
|
80
|
-
} else if (matches === undefined || matches(absolutePathToFile)) {
|
|
80
|
+
} else if (matches === undefined || (await matches(absolutePathToFile))) {
|
|
81
81
|
return absolutePathToFile;
|
|
82
82
|
} else {
|
|
83
83
|
return [];
|
|
@@ -93,7 +93,7 @@ export async function getAllFilesMatching(
|
|
|
93
93
|
* satisfy the specified condition, returning their absolute paths. Once a
|
|
94
94
|
* directory is found, its subdirectories are not searched.
|
|
95
95
|
*
|
|
96
|
-
* Note: dirFrom is never returned, nor `matches`
|
|
96
|
+
* Note: dirFrom is never returned, nor is `matches` called on it.
|
|
97
97
|
*
|
|
98
98
|
* @param dirFrom The absolute path of the directory to start the search from.
|
|
99
99
|
* @param matches A function to filter directories (not files).
|
|
@@ -105,7 +105,7 @@ export async function getAllFilesMatching(
|
|
|
105
105
|
*/
|
|
106
106
|
export async function getAllDirectoriesMatching(
|
|
107
107
|
dirFrom: string,
|
|
108
|
-
matches?: (absolutePathToDir: string) => boolean,
|
|
108
|
+
matches?: (absolutePathToDir: string) => Promise<boolean> | boolean,
|
|
109
109
|
): Promise<string[]> {
|
|
110
110
|
const dirContent = await readdirOrEmpty(dirFrom);
|
|
111
111
|
|
|
@@ -116,7 +116,7 @@ export async function getAllDirectoriesMatching(
|
|
|
116
116
|
return [];
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
if (matches === undefined || matches(absolutePathToFile)) {
|
|
119
|
+
if (matches === undefined || (await matches(absolutePathToFile))) {
|
|
120
120
|
return absolutePathToFile;
|
|
121
121
|
}
|
|
122
122
|
|
|
@@ -540,7 +540,9 @@ export const ensureDir: typeof mkdir = mkdir;
|
|
|
540
540
|
*/
|
|
541
541
|
export async function mkdtemp(prefix: string): Promise<string> {
|
|
542
542
|
try {
|
|
543
|
-
return await
|
|
543
|
+
return await getRealPath(
|
|
544
|
+
await fsPromises.mkdtemp(path.join(tmpdir(), prefix)),
|
|
545
|
+
);
|
|
544
546
|
} catch (e) {
|
|
545
547
|
ensureNodeErrnoExceptionError(e);
|
|
546
548
|
throw new FileSystemAccessError(e.message, e);
|
|
@@ -832,6 +834,39 @@ export async function findUp(
|
|
|
832
834
|
}
|
|
833
835
|
}
|
|
834
836
|
|
|
837
|
+
/**
|
|
838
|
+
* This function uses some heuristics to check if a file is binary by reading the first bytesToCheck bytes from the file.
|
|
839
|
+
*/
|
|
840
|
+
export async function isBinaryFile(
|
|
841
|
+
filePath: string,
|
|
842
|
+
bytesToCheck = 8000,
|
|
843
|
+
): Promise<boolean> {
|
|
844
|
+
const fd = await fsPromises.open(filePath, "r");
|
|
845
|
+
|
|
846
|
+
const buffer = Buffer.alloc(bytesToCheck);
|
|
847
|
+
const { bytesRead } = await fd.read(buffer, 0, bytesToCheck, 0);
|
|
848
|
+
await fd.close();
|
|
849
|
+
|
|
850
|
+
let nonPrintable = 0;
|
|
851
|
+
for (let i = 0; i < bytesRead; i++) {
|
|
852
|
+
const byte = buffer[i];
|
|
853
|
+
|
|
854
|
+
// Allow common text ranges: tab, newline, carriage return, and printable ASCII
|
|
855
|
+
if (
|
|
856
|
+
byte === 9 || // tab
|
|
857
|
+
byte === 10 || // newline
|
|
858
|
+
byte === 13 || // carriage return
|
|
859
|
+
(byte >= 32 && byte <= 126)
|
|
860
|
+
) {
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
nonPrintable++;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Heuristic: if more than ~30% of bytes are non-printable, assume binary
|
|
867
|
+
return nonPrintable / bytesRead > 0.3;
|
|
868
|
+
}
|
|
869
|
+
|
|
835
870
|
export {
|
|
836
871
|
FileNotFoundError,
|
|
837
872
|
FileSystemAccessError,
|
package/src/internal/bytecode.ts
CHANGED
package/src/lang.ts
CHANGED
|
@@ -71,3 +71,39 @@ export function isObject(
|
|
|
71
71
|
export async function sleep(seconds: number): Promise<void> {
|
|
72
72
|
await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
|
|
73
73
|
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Binds all methods of an object to the object itself, so that they can be
|
|
77
|
+
* assigned to an independent variable and still work.
|
|
78
|
+
*
|
|
79
|
+
* @param obj The object, which can be an instance of a class.
|
|
80
|
+
*/
|
|
81
|
+
export function bindAllMethods<ObjectT extends object>(obj: ObjectT): void {
|
|
82
|
+
const prototype = Object.getPrototypeOf(obj);
|
|
83
|
+
const prototypeKeys =
|
|
84
|
+
prototype !== null ? Object.getOwnPropertyNames(prototype) : [];
|
|
85
|
+
|
|
86
|
+
const keys = [...prototypeKeys, ...Object.getOwnPropertyNames(obj)];
|
|
87
|
+
|
|
88
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions --
|
|
89
|
+
typescript can't express this in a safe way, so we use any here */
|
|
90
|
+
const objAsAny = obj as any;
|
|
91
|
+
|
|
92
|
+
// Exclude methods that should not be rebound (constructor, Object.prototype methods, etc.)
|
|
93
|
+
const EXCLUDED_METHODS = [
|
|
94
|
+
"constructor",
|
|
95
|
+
"hasOwnProperty",
|
|
96
|
+
"isPrototypeOf",
|
|
97
|
+
"propertyIsEnumerable",
|
|
98
|
+
"toLocaleString",
|
|
99
|
+
"toString",
|
|
100
|
+
"valueOf",
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
for (const key of keys) {
|
|
104
|
+
const val = objAsAny[key];
|
|
105
|
+
if (typeof val === "function" && !EXCLUDED_METHODS.includes(key)) {
|
|
106
|
+
objAsAny[key] = val.bind(obj);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
package/src/package.ts
CHANGED
|
@@ -10,6 +10,31 @@ import { exists, findUp, getRealPath, readJsonFile } from "./fs.js";
|
|
|
10
10
|
import { getFilePath } from "./internal/package.js";
|
|
11
11
|
import { ensureTrailingSlash } from "./string.js";
|
|
12
12
|
|
|
13
|
+
/* Adapted from `resolve.exports`. License: https://github.com/lukeed/resolve.exports/blob/master/license */
|
|
14
|
+
|
|
15
|
+
export type PackageExports =
|
|
16
|
+
| PackageExportPath
|
|
17
|
+
| {
|
|
18
|
+
[path: PackageExportsEntry]: PackageExportsValue;
|
|
19
|
+
[condition: string]: PackageExportsValue;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Allows "." and "./{name}" */
|
|
23
|
+
export type PackageExportsEntry = `.${string}`;
|
|
24
|
+
|
|
25
|
+
/** Internal path */
|
|
26
|
+
export type PackageExportPath = `./${string}`;
|
|
27
|
+
|
|
28
|
+
export type PackageExportsValue =
|
|
29
|
+
| PackageExportPath
|
|
30
|
+
| null
|
|
31
|
+
| {
|
|
32
|
+
[condition: string]: PackageExportsValue;
|
|
33
|
+
}
|
|
34
|
+
| PackageExportsValue[];
|
|
35
|
+
|
|
36
|
+
/* End of `resolve.exports` adaptation */
|
|
37
|
+
|
|
13
38
|
/**
|
|
14
39
|
* The structure of a `package.json` file. This is a subset of the actual
|
|
15
40
|
* `package.json` file, if you need to access other fields you add them here.
|
|
@@ -22,6 +47,7 @@ export interface PackageJson {
|
|
|
22
47
|
engines?: {
|
|
23
48
|
node?: string;
|
|
24
49
|
};
|
|
50
|
+
exports?: PackageExports;
|
|
25
51
|
dependencies?: Record<string, string>;
|
|
26
52
|
devDependencies?: Record<string, string>;
|
|
27
53
|
peerDependencies?: Record<string, string>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { numberToHexString } from "./hex.js";
|
|
2
|
+
|
|
3
|
+
export function panicErrorCodeToMessage(errorCode: bigint): string {
|
|
4
|
+
const reason = panicErrorCodeToReason(errorCode);
|
|
5
|
+
|
|
6
|
+
if (reason !== undefined) {
|
|
7
|
+
return `reverted with panic code ${numberToHexString(errorCode)} (${reason})`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return `reverted with unknown panic code ${numberToHexString(errorCode)}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function panicErrorCodeToReason(errorCode: bigint): string | undefined {
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check -- we are only covering some of the integer range
|
|
15
|
+
switch (errorCode) {
|
|
16
|
+
case 0x1n:
|
|
17
|
+
return "Assertion error";
|
|
18
|
+
case 0x11n:
|
|
19
|
+
return "Arithmetic operation overflowed outside of an unchecked block";
|
|
20
|
+
case 0x12n:
|
|
21
|
+
return "Division or modulo division by zero";
|
|
22
|
+
case 0x21n:
|
|
23
|
+
return "Tried to convert a value into an enum, but the value was too big or negative";
|
|
24
|
+
case 0x22n:
|
|
25
|
+
return "Incorrectly encoded storage byte array";
|
|
26
|
+
case 0x31n:
|
|
27
|
+
return ".pop() was called on an empty array";
|
|
28
|
+
case 0x32n:
|
|
29
|
+
return "Array accessed at an out-of-bounds or negative index";
|
|
30
|
+
case 0x41n:
|
|
31
|
+
return "Too much memory was allocated, or an array was created that is too large";
|
|
32
|
+
case 0x51n:
|
|
33
|
+
return "Called a zero-initialized variable of internal function type";
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/path.ts
CHANGED
|
@@ -22,7 +22,7 @@ export function resolveFromRoot(root: string, target: string): string {
|
|
|
22
22
|
* Tries to return a shorter version of the path if its inside the given folder.
|
|
23
23
|
*
|
|
24
24
|
* This is useful for displaying paths in the terminal, as they can be shorter
|
|
25
|
-
* when they are
|
|
25
|
+
* when they are inside the current working directory. For example, if the
|
|
26
26
|
* current working directory is `/home/user/project`, and the path is
|
|
27
27
|
* `/home/user/project/contracts/File.sol`, the shorter path is
|
|
28
28
|
* `contracts/File.sol`.
|
|
@@ -31,11 +31,21 @@ export function resolveFromRoot(root: string, target: string): string {
|
|
|
31
31
|
* @param folder The absolute path to the folder.
|
|
32
32
|
* @returns The shorter path, if possible, or the original path.
|
|
33
33
|
*/
|
|
34
|
-
export function shortenPath(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
export function shortenPath(absolutePath: string): string {
|
|
35
|
+
const cwd = process.cwd();
|
|
36
|
+
let relativePath = path.relative(cwd, absolutePath);
|
|
37
|
+
|
|
38
|
+
if (relativePath === "..") {
|
|
39
|
+
return ".." + path.sep;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (
|
|
43
|
+
!relativePath.startsWith(".." + path.sep) &&
|
|
44
|
+
!relativePath.startsWith("." + path.sep) &&
|
|
45
|
+
!path.isAbsolute(relativePath)
|
|
46
|
+
) {
|
|
47
|
+
relativePath = "." + path.sep + relativePath;
|
|
48
|
+
}
|
|
39
49
|
|
|
40
50
|
if (relativePath.length < absolutePath.length) {
|
|
41
51
|
return relativePath;
|
package/src/request.ts
CHANGED
|
@@ -62,13 +62,22 @@ export interface RequestOptions {
|
|
|
62
62
|
abortSignal?: AbortSignal | EventEmitter;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
export interface HttpResponse {
|
|
66
|
+
statusCode: number;
|
|
67
|
+
body: {
|
|
68
|
+
json(): Promise<any>;
|
|
69
|
+
text(): Promise<string>;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
65
73
|
/**
|
|
66
74
|
* Performs a HTTP request.
|
|
67
75
|
*
|
|
68
76
|
* @param url The url to make the request to.
|
|
69
77
|
* @param requestOptions The options to configure the request. See {@link RequestOptions}.
|
|
70
78
|
* @param dispatcherOrDispatcherOptions Either a dispatcher or dispatcher options. See {@link DispatcherOptions}.
|
|
71
|
-
* @returns
|
|
79
|
+
* @returns An object containing the status code and the response body. The body can be accessed as JSON or text.
|
|
80
|
+
* `body` can not be consumed twice. For example, calling `text()` after `json()` throws `TypeError`.
|
|
72
81
|
* @throws ConnectionRefusedError If the connection is refused by the server.
|
|
73
82
|
* @throws RequestTimeoutError If the request times out.
|
|
74
83
|
* @throws RequestError If the request fails for any other reason.
|
|
@@ -77,7 +86,7 @@ export async function getRequest(
|
|
|
77
86
|
url: string,
|
|
78
87
|
requestOptions: RequestOptions = {},
|
|
79
88
|
dispatcherOrDispatcherOptions?: UndiciT.Dispatcher | DispatcherOptions,
|
|
80
|
-
): Promise<
|
|
89
|
+
): Promise<HttpResponse> {
|
|
81
90
|
const { request } = await import("undici");
|
|
82
91
|
|
|
83
92
|
try {
|
|
@@ -106,7 +115,8 @@ export async function getRequest(
|
|
|
106
115
|
* @param body The body of the request, represented as an object.
|
|
107
116
|
* @param requestOptions The options to configure the request. See {@link RequestOptions}.
|
|
108
117
|
* @param dispatcherOrDispatcherOptions Either a dispatcher or dispatcher options. See {@link DispatcherOptions}.
|
|
109
|
-
* @returns
|
|
118
|
+
* @returns An object containing the status code and the response body. The body can be accessed as JSON or text.
|
|
119
|
+
* `body` can not be consumed twice. For example, calling `text()` after `json()` throws `TypeError`.
|
|
110
120
|
* @throws ConnectionRefusedError If the connection is refused by the server.
|
|
111
121
|
* @throws RequestTimeoutError If the request times out.
|
|
112
122
|
* @throws RequestError If the request fails for any other reason.
|
|
@@ -116,7 +126,7 @@ export async function postJsonRequest(
|
|
|
116
126
|
body: unknown,
|
|
117
127
|
requestOptions: RequestOptions = {},
|
|
118
128
|
dispatcherOrDispatcherOptions?: UndiciT.Dispatcher | DispatcherOptions,
|
|
119
|
-
): Promise<
|
|
129
|
+
): Promise<HttpResponse> {
|
|
120
130
|
const { request } = await import("undici");
|
|
121
131
|
|
|
122
132
|
try {
|
|
@@ -150,7 +160,8 @@ export async function postJsonRequest(
|
|
|
150
160
|
* @param body The body of the request, represented as an object.
|
|
151
161
|
* @param requestOptions The options to configure the request. See {@link RequestOptions}.
|
|
152
162
|
* @param dispatcherOrDispatcherOptions Either a dispatcher or dispatcher options. See {@link DispatcherOptions}.
|
|
153
|
-
* @returns
|
|
163
|
+
* @returns An object containing the status code and the response body. The body can be accessed as JSON or text.
|
|
164
|
+
* `body` can not be consumed twice. For example, calling `text()` after `json()` throws `TypeError`.
|
|
154
165
|
* @throws ConnectionRefusedError If the connection is refused by the server.
|
|
155
166
|
* @throws RequestTimeoutError If the request times out.
|
|
156
167
|
* @throws RequestError If the request fails for any other reason.
|
|
@@ -160,7 +171,7 @@ export async function postFormRequest(
|
|
|
160
171
|
body: unknown,
|
|
161
172
|
requestOptions: RequestOptions = {},
|
|
162
173
|
dispatcherOrDispatcherOptions?: UndiciT.Dispatcher | DispatcherOptions,
|
|
163
|
-
): Promise<
|
|
174
|
+
): Promise<HttpResponse> {
|
|
164
175
|
const { request } = await import("undici");
|
|
165
176
|
|
|
166
177
|
try {
|
|
@@ -208,11 +219,16 @@ export async function download(
|
|
|
208
219
|
let statusCode: number | undefined;
|
|
209
220
|
|
|
210
221
|
try {
|
|
211
|
-
|
|
222
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
223
|
+
-- We need the full Dispatcher.ResponseData here for stream.pipeline,
|
|
224
|
+
but HttpResponse doesn’t expose the raw ReadableStream.
|
|
225
|
+
TODO: wrap undici's request so we can keep the public API
|
|
226
|
+
strictly typed without falling back to Undici types. */
|
|
227
|
+
const response = (await getRequest(
|
|
212
228
|
url,
|
|
213
229
|
requestOptions,
|
|
214
230
|
dispatcherOrDispatcherOptions,
|
|
215
|
-
);
|
|
231
|
+
)) as UndiciT.Dispatcher.ResponseData;
|
|
216
232
|
const { body } = response;
|
|
217
233
|
statusCode = response.statusCode;
|
|
218
234
|
|
|
@@ -331,6 +347,37 @@ export function isValidUrl(url: string): boolean {
|
|
|
331
347
|
}
|
|
332
348
|
}
|
|
333
349
|
|
|
350
|
+
/**
|
|
351
|
+
* Returns the proxy URL from environment variables based on the target URL.
|
|
352
|
+
* For HTTPS URLs, checks `https_proxy` then `HTTPS_PROXY`.
|
|
353
|
+
* For HTTP URLs, checks `http_proxy` then `HTTP_PROXY`.
|
|
354
|
+
* Falls back to the other protocol's proxy if none found.
|
|
355
|
+
*
|
|
356
|
+
* @param url The target URL to determine proxy for.
|
|
357
|
+
* @returns The proxy URL, or `undefined` if none are set.
|
|
358
|
+
*/
|
|
359
|
+
export function getProxyUrl(url: string): string | undefined {
|
|
360
|
+
const { protocol } = new URL(url);
|
|
361
|
+
|
|
362
|
+
if (protocol === "https:") {
|
|
363
|
+
return (
|
|
364
|
+
process.env.https_proxy ??
|
|
365
|
+
process.env.HTTPS_PROXY ??
|
|
366
|
+
process.env.http_proxy ??
|
|
367
|
+
process.env.HTTP_PROXY
|
|
368
|
+
);
|
|
369
|
+
} else if (protocol === "http:") {
|
|
370
|
+
return (
|
|
371
|
+
process.env.http_proxy ??
|
|
372
|
+
process.env.HTTP_PROXY ??
|
|
373
|
+
process.env.https_proxy ??
|
|
374
|
+
process.env.HTTPS_PROXY
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
380
|
+
|
|
334
381
|
export {
|
|
335
382
|
ConnectionRefusedError,
|
|
336
383
|
DispatcherError,
|
package/src/stream.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Writable } from "node:stream";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Creates a
|
|
4
|
+
* Creates a Transform that writes everything to actualWritable, without closing it
|
|
5
5
|
* when finished.
|
|
6
6
|
*
|
|
7
7
|
* This is useful to pipe things to stdout, without closing it, while being
|
package/src/subprocess.ts
CHANGED
|
@@ -33,7 +33,7 @@ export async function spawnDetachedSubProcess(
|
|
|
33
33
|
const subprocessArgs = [absolutePathToSubProcessFile, ...args];
|
|
34
34
|
|
|
35
35
|
if (absolutePathToSubProcessFile.endsWith(".ts")) {
|
|
36
|
-
subprocessArgs.unshift("--import", "tsx/esm");
|
|
36
|
+
subprocessArgs.unshift("--import", import.meta.resolve("tsx/esm"));
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
const subprocess = spawn(process.execPath, subprocessArgs, {
|
package/src/synchronization.ts
CHANGED
|
@@ -15,6 +15,7 @@ import debug from "debug";
|
|
|
15
15
|
|
|
16
16
|
import { ensureNodeErrnoExceptionError } from "./error.js";
|
|
17
17
|
import { FileSystemAccessError } from "./errors/fs.js";
|
|
18
|
+
import { readUtf8File } from "./fs.js";
|
|
18
19
|
import { sleep } from "./lang.js";
|
|
19
20
|
|
|
20
21
|
const log = debug("hardhat:util:multi-process-mutex");
|
|
@@ -48,6 +49,13 @@ export class MultiProcessMutex {
|
|
|
48
49
|
log(
|
|
49
50
|
`Current mutex file is too old, removing it at path '${this.#mutexFilePath}'`,
|
|
50
51
|
);
|
|
52
|
+
|
|
53
|
+
this.#deleteMutexFile();
|
|
54
|
+
} else if (await this.#isMutexProcessOwnerDead()) {
|
|
55
|
+
log(
|
|
56
|
+
`The process owning the mutex file no longer exists. Removing mutex file at '${this.#mutexFilePath}'.`,
|
|
57
|
+
);
|
|
58
|
+
|
|
51
59
|
this.#deleteMutexFile();
|
|
52
60
|
} else {
|
|
53
61
|
// wait
|
|
@@ -59,7 +67,10 @@ export class MultiProcessMutex {
|
|
|
59
67
|
async #tryToAcquireMutex() {
|
|
60
68
|
try {
|
|
61
69
|
// Create a file only if it does not exist
|
|
62
|
-
fs.writeFileSync(this.#mutexFilePath,
|
|
70
|
+
fs.writeFileSync(this.#mutexFilePath, process.pid.toString(), {
|
|
71
|
+
flag: "wx+",
|
|
72
|
+
});
|
|
73
|
+
|
|
63
74
|
return true;
|
|
64
75
|
} catch (e) {
|
|
65
76
|
ensureNodeErrnoExceptionError(e);
|
|
@@ -80,6 +91,8 @@ export class MultiProcessMutex {
|
|
|
80
91
|
return await f();
|
|
81
92
|
} finally {
|
|
82
93
|
// Release the mutex
|
|
94
|
+
// Note: if a process dies, its `finally` block never executes, and the process hangs indefinitely since no response is received.
|
|
95
|
+
// To handle this, we use the function `isMutexProcessOwnerDead`.
|
|
83
96
|
log(`Mutex released at path '${this.#mutexFilePath}'`);
|
|
84
97
|
this.#deleteMutexFile();
|
|
85
98
|
log(`Mutex released at path '${this.#mutexFilePath}'`);
|
|
@@ -123,4 +136,80 @@ export class MultiProcessMutex {
|
|
|
123
136
|
throw new FileSystemAccessError(e.message, e);
|
|
124
137
|
}
|
|
125
138
|
}
|
|
139
|
+
|
|
140
|
+
async #isMutexProcessOwnerDead(): Promise<boolean> {
|
|
141
|
+
let mutexPid: string;
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
// If the file doesn't exist, it means the owning process deleted it
|
|
145
|
+
mutexPid = await readUtf8File(this.#mutexFilePath);
|
|
146
|
+
} catch (_e) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
process.kill(parseInt(mutexPid, 10), 0);
|
|
152
|
+
} catch (e) {
|
|
153
|
+
ensureNodeErrnoExceptionError(e);
|
|
154
|
+
|
|
155
|
+
if (e.code === "ESRCH") {
|
|
156
|
+
// The process owning the mutex no longer exists
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* A class that implements an asynchronous mutex (mutual exclusion) lock.
|
|
167
|
+
*
|
|
168
|
+
* The mutex ensures that only one asynchronous operation can be executed at a time,
|
|
169
|
+
* providing exclusive access to a shared resource.
|
|
170
|
+
*/
|
|
171
|
+
export class AsyncMutex {
|
|
172
|
+
#acquired = false;
|
|
173
|
+
readonly #queue: Array<() => void> = [];
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Acquires the mutex, running the provided function exclusively,
|
|
177
|
+
* and releasing it afterwards.
|
|
178
|
+
*
|
|
179
|
+
* @param f The function to run.
|
|
180
|
+
* @returns The result of the function.
|
|
181
|
+
*/
|
|
182
|
+
public async exclusiveRun<ReturnT>(
|
|
183
|
+
f: () => ReturnT,
|
|
184
|
+
): Promise<Awaited<ReturnT>> {
|
|
185
|
+
const release = await this.#acquire();
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
return await f();
|
|
189
|
+
} finally {
|
|
190
|
+
await release();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Acquires the mutex, returning a function that releases it.
|
|
196
|
+
*/
|
|
197
|
+
async #acquire(): Promise<() => Promise<void>> {
|
|
198
|
+
if (!this.#acquired) {
|
|
199
|
+
this.#acquired = true;
|
|
200
|
+
return async () => {
|
|
201
|
+
this.#acquired = false;
|
|
202
|
+
const next = this.#queue.shift();
|
|
203
|
+
if (next !== undefined) {
|
|
204
|
+
next();
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return new Promise<() => Promise<void>>((resolve) => {
|
|
210
|
+
this.#queue.push(() => {
|
|
211
|
+
resolve(this.#acquire());
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
}
|
|
126
215
|
}
|