-
Notifications
You must be signed in to change notification settings - Fork 56
Add generic environment-creation utilities (PEP 723 PR 5a/16) #1651
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| import * as crypto from 'crypto'; | ||
| import * as fsapi from 'fs-extra'; | ||
| import * as path from 'path'; | ||
|
|
||
| export interface AcquireFileLockOptions { | ||
| readonly timeoutMs: number; | ||
| readonly retryIntervalMs: number; | ||
| } | ||
|
|
||
| export interface AcquiredFileLock { | ||
| readonly release: () => Promise<void>; | ||
| /** Keep the lock and make later acquisition attempts fail immediately. */ | ||
| readonly retain: () => Promise<void>; | ||
| } | ||
|
|
||
| type LockState = 'held' | 'released' | 'retained'; | ||
|
|
||
| /** Acquire an atomic lock released only explicitly; interrupted operations remain locked. */ | ||
| export async function acquireFileLock(filePath: string, options: AcquireFileLockOptions): Promise<AcquiredFileLock> { | ||
| const lockPath = `${path.resolve(filePath)}.lock`; | ||
| const ownerMarker = path.join(lockPath, `owner-${process.pid}-${crypto.randomBytes(16).toString('hex')}`); | ||
| const retainedMarker = path.join(lockPath, 'retained'); | ||
| const deadline = Date.now() + options.timeoutMs; | ||
|
|
||
| while (true) { | ||
| try { | ||
| await fsapi.mkdir(lockPath); | ||
| try { | ||
| await fsapi.writeFile(ownerMarker, '', { flag: 'wx' }); | ||
| } catch (error) { | ||
| try { | ||
| await fsapi.rmdir(lockPath); | ||
| } catch { | ||
| throw createLockError( | ||
| 'Lock initialization failed and left an owner-less lock directory', | ||
| 'ELOCKORPHANED', | ||
| lockPath, | ||
| ); | ||
| } | ||
| throw error; | ||
| } | ||
|
|
||
| let state: LockState = 'held'; | ||
| return { | ||
| retain: async () => { | ||
| if (state !== 'held') { | ||
| return; | ||
| } | ||
| state = 'retained'; | ||
| try { | ||
| await fsapi.writeFile(retainedMarker, '', { flag: 'wx' }); | ||
| } catch (error) { | ||
| if (hasErrorCode(error, 'EEXIST')) { | ||
| return; | ||
| } | ||
| try { | ||
| await fsapi.rename(ownerMarker, retainedMarker); | ||
| } catch (renameError) { | ||
| if (!hasErrorCode(renameError, 'EEXIST')) { | ||
| throw createLockError( | ||
| 'Failed to mark the lock as retained', | ||
| 'ERETAINFAILED', | ||
| lockPath, | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| release: async () => { | ||
| if (state !== 'held') { | ||
| return; | ||
| } | ||
| state = 'released'; | ||
| try { | ||
| await fsapi.unlink(ownerMarker); | ||
| } catch (error) { | ||
| if (hasErrorCode(error, 'ENOENT')) { | ||
| throw createLockError('Lock ownership was compromised', 'ECOMPROMISED', lockPath); | ||
| } | ||
| throw error; | ||
| } | ||
| await fsapi.rmdir(lockPath); | ||
| }, | ||
| }; | ||
| } catch (error) { | ||
| if (!hasErrorCode(error, 'EEXIST')) { | ||
| throw error; | ||
| } | ||
| if (await isRetainedLock(lockPath)) { | ||
| throw createLockError('Lock was retained after an interrupted operation', 'ELOCKRETAINED', lockPath); | ||
| } | ||
| if (Date.now() >= deadline) { | ||
| throw createLockError('Lock is already being held', 'ELOCKED', lockPath); | ||
| } | ||
| await delay(Math.min(options.retryIntervalMs, Math.max(0, deadline - Date.now()))); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| async function isRetainedLock(lockPath: string): Promise<boolean> { | ||
| try { | ||
| await fsapi.lstat(path.join(lockPath, 'retained')); | ||
| return true; | ||
| } catch (error) { | ||
| if (hasErrorCode(error, 'ENOENT')) { | ||
| return false; | ||
| } | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| function hasErrorCode(error: unknown, code: string): boolean { | ||
| return ( | ||
| typeof error === 'object' && error !== null && 'code' in error && (error as NodeJS.ErrnoException).code === code | ||
| ); | ||
| } | ||
|
|
||
| function createLockError(message: string, code: string, lockPath: string): NodeJS.ErrnoException { | ||
| return Object.assign(new Error(message), { code, path: lockPath }); | ||
| } | ||
|
|
||
| async function delay(milliseconds: number): Promise<void> { | ||
| return new Promise((resolve) => setTimeout(resolve, milliseconds)); | ||
| } | ||
|
Comment on lines
+125
to
+127
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe this could be moved to a more generic location? But it doesn't bother me too much. |
||
|
StellaHuang95 marked this conversation as resolved.
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| import * as path from 'path'; | ||
| import { isWindows } from './platformUtils'; | ||
|
|
||
| export function getVenvPythonPath(envPath: string): string { | ||
| return isWindows() | ||
| ? path.join(envPath, 'Scripts', 'python.exe') | ||
| : path.join(envPath, 'bin', 'python'); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -73,12 +73,22 @@ export async function runUV( | |
| spawnOptions.timeout = timeout; | ||
| } | ||
| const proc = spawnProcess('uv', args, spawnOptions); | ||
| let cancellationRequested = false; | ||
| token?.onCancellationRequested(() => { | ||
| proc.kill(); | ||
| cancellationRequested = true; | ||
| try { | ||
| proc.kill(); | ||
| } catch { | ||
| // Preserve cancellation when signaling fails. | ||
| } | ||
| reject(new CancellationError()); | ||
| }); | ||
|
|
||
| proc.on('error', (err) => { | ||
| if (cancellationRequested) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is cleanup required by this error stage? you went through and confirmed we aren't leaving stuff behind in people's file systems right?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change I made only catches |
||
| reject(new CancellationError()); | ||
| return; | ||
| } | ||
| log?.error(`Error spawning uv: ${err}`); | ||
| reject(new Error(`Error spawning uv: ${err.message}`)); | ||
| }); | ||
|
|
@@ -114,12 +124,22 @@ export async function runPython( | |
| log?.info(`Running: ${python} ${args.join(' ')}`); | ||
| return new Promise<string>((resolve, reject) => { | ||
| const proc = spawnProcess(python, args, { cwd: cwd, timeout }); | ||
| let cancellationRequested = false; | ||
| token?.onCancellationRequested(() => { | ||
| proc.kill(); | ||
| cancellationRequested = true; | ||
| try { | ||
| proc.kill(); | ||
| } catch { | ||
| // Preserve cancellation when signaling fails. | ||
| } | ||
| reject(new CancellationError()); | ||
| }); | ||
|
|
||
| proc.on('error', (err) => { | ||
| if (cancellationRequested) { | ||
| reject(new CancellationError()); | ||
| return; | ||
| } | ||
| log?.error(`Error spawning python: ${err}`); | ||
| reject(new Error(`Error spawning python: ${err.message}`)); | ||
| }); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.