Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/chilled-ads-exist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@nomicfoundation/hardhat-utils": major
"hardhat": patch
---

Introduce new inter-process mutex implementation ([7942](https://github.com/NomicFoundation/hardhat/pull/7942)).
5 changes: 5 additions & 0 deletions .changeset/six-taxes-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nomicfoundation/hardhat-utils": patch
---

Fix two issues in the `download` function ([7942](https://github.com/NomicFoundation/hardhat/pull/7942)).
5 changes: 5 additions & 0 deletions .changeset/slick-donuts-carry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hardhat": patch
---

Make the solc downloader safe when run by multiple processes ([7946](https://github.com/NomicFoundation/hardhat/pull/7946)).
75 changes: 75 additions & 0 deletions v-next/hardhat-utils/src/errors/synchronization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { CustomError } from "../error.js";

export class BaseMultiProcessMutexError extends CustomError {
constructor(message: string, cause?: Error) {
super(message, cause);
}
}

export class InvalidMultiProcessMutexPathError extends BaseMultiProcessMutexError {
constructor(mutexPath: string) {
super(`The path ${mutexPath} is not a valid absolute path.`);
}
}

export class MultiProcessMutexError extends BaseMultiProcessMutexError {
constructor(lockPath: string, cause: Error) {
super(`Unexpected error with lock at ${lockPath}: ${cause.message}`, cause);
}
}

export class MultiProcessMutexTimeoutError extends BaseMultiProcessMutexError {
constructor(lockPath: string, timeoutMs: number) {
super(
`Timed out waiting to acquire lock at ${lockPath} after ${timeoutMs}ms`,
);
}
}

export class StaleMultiProcessMutexError extends BaseMultiProcessMutexError {
constructor(lockPath: string, ownerUid: number | undefined, cause: Error) {
const uidInfo = ownerUid !== undefined ? ` (uid: ${ownerUid})` : "";
super(
`Lock at ${lockPath} appears stale but cannot be removed due to insufficient permissions${uidInfo}. Please remove it manually: ${lockPath}`,
cause,
);
}
}

export class IncompatibleMultiProcessMutexError extends BaseMultiProcessMutexError {
constructor(message: string) {
super(message);
}
}

export class IncompatibleHostnameMultiProcessMutexError extends IncompatibleMultiProcessMutexError {
constructor(
lockPath: string,
foreignHostname: string,
currentHostname: string,
) {
super(
`Lock at ${lockPath} was created by a different host (${foreignHostname}, current: ${currentHostname}). It cannot be verified or removed automatically. Please remove it manually: ${lockPath}`,
);
}
}

export class IncompatiblePlatformMultiProcessMutexError extends IncompatibleMultiProcessMutexError {
constructor(
lockPath: string,
foreignPlatform: string,
currentPlatform: string,
) {
super(
`Lock at ${lockPath} was created on a different platform (${foreignPlatform}, current: ${currentPlatform}). It cannot be verified or removed automatically. Please remove it manually: ${lockPath}`,
);
}
}

export class IncompatibleUidMultiProcessMutexError extends IncompatibleMultiProcessMutexError {
constructor(lockPath: string, foreignUid: number, currentUid: number) {
super(
`Lock at ${lockPath} is owned by a different user (uid: ${foreignUid}, current: ${currentUid}). It cannot be removed automatically. Please remove it manually: ${lockPath}`,
);
}
}
3 changes: 2 additions & 1 deletion v-next/hardhat-utils/src/internal/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { DispatcherOptions, RequestOptions } from "../request.js";
import type EventEmitter from "node:events";
import type UndiciT from "undici";

import crypto from "node:crypto";
import path from "node:path";

import { mkdir } from "../fs.js";
Expand All @@ -24,7 +25,7 @@ export async function generateTempFilePath(filePath: string): Promise<string> {
return path.format({
dir,
ext,
name: `tmp-${name}`,
name: `tmp-${name}-${crypto.randomBytes(8).toString("hex")}`,
});
}

Expand Down
35 changes: 29 additions & 6 deletions v-next/hardhat-utils/src/request.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type EventEmitter from "node:events";
import type { FileHandle } from "node:fs/promises";
import type { ParsedUrlQueryInput } from "node:querystring";
import type UndiciT from "undici";

import fs from "node:fs";
import { open } from "node:fs/promises";
import querystring from "node:querystring";
import stream from "node:stream/promises";

Expand All @@ -12,7 +13,7 @@ import {
RequestError,
DispatcherError,
} from "./errors/request.js";
import { move } from "./fs.js";
import { move, remove } from "./fs.js";
import {
generateTempFilePath,
getBaseDispatcherOptions,
Expand Down Expand Up @@ -217,11 +218,12 @@ export async function download(
dispatcherOrDispatcherOptions?: UndiciT.Dispatcher | DispatcherOptions,
): Promise<void> {
let statusCode: number | undefined;
let tempFilePath: string | undefined;

try {
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions
-- We need the full Dispatcher.ResponseData here for stream.pipeline,
but HttpResponse doesnt expose the raw ReadableStream.
but HttpResponse doesn't expose the raw ReadableStream.
TODO: wrap undici's request so we can keep the public API
strictly typed without falling back to Undici types. */
const response = (await getRequest(
Expand All @@ -236,13 +238,34 @@ export async function download(
throw new Error(await body.text());
}

const tempFilePath = await generateTempFilePath(destination);
const fileStream = fs.createWriteStream(tempFilePath);
await stream.pipeline(body, fileStream);
tempFilePath = await generateTempFilePath(destination);

let fileHandle: FileHandle | undefined;

try {
fileHandle = await open(tempFilePath, "w");

const fileStream = fileHandle.createWriteStream();

await stream.pipeline(body, fileStream);
} finally {
// NOTE: Historically, not closing the file handle caused issues on Windows,
// for example, when trying to move the file previously written to by this function
await fileHandle?.close();
}

await move(tempFilePath, destination);
} catch (e) {
ensureError(e);

if (tempFilePath !== undefined) {
try {
await remove(tempFilePath);
} catch {
// Best-effort: file may not exist or may have already been moved
}
}

handleError(e, url);

throw new DownloadError(url, e);
Expand Down
Loading