diff --git a/application/holder/src/env/os/index.ts b/application/holder/src/env/os/index.ts index f306008eb..a75bbadf6 100644 --- a/application/holder/src/env/os/index.ts +++ b/application/holder/src/env/os/index.ts @@ -2,10 +2,6 @@ import { exec, spawn } from 'child_process'; import * as os from 'os'; -import { detectAvailableProfiles as getProfiles, ITerminalProfile } from './profiles'; - -export { getProfiles, ITerminalProfile }; - export function shell(command: string, defShell?: string): Promise { return new Promise((resolve, reject) => { exec( @@ -40,43 +36,6 @@ export enum EPlatforms { export type TEnvVars = { [key: string]: string }; -export function printenv(shellFullPath?: string): Promise { - if (os.platform() === EPlatforms.win32) { - return Promise.reject(new Error(`This command doesn't supported by windows.`)); - } - return new Promise((resolve, reject) => { - (() => { - if (shellFullPath === undefined) { - return getDefShell(); - } else { - return Promise.resolve(shellFullPath); - } - })() - .then((defShell: string) => { - shell('printenv', defShell) - .then((stdout: string) => { - const pairs: TEnvVars = {}; - stdout.split(/[\n\r]/gi).forEach((row: string) => { - const pair: string[] = row.split('='); - if (pair.length <= 1) { - return; - } - pairs[pair[0]] = row.replace(`${pair[0]}=`, ''); - }); - if (Object.keys(pairs).length === 0) { - return resolve(Object.assign({}, process.env) as TEnvVars); - } - resolve(pairs); - }) - .catch((error: Error) => { - reject(error); - }); - }) - .catch((defShellErr: Error) => { - reject(defShellErr); - }); - }); -} export function getElectronAppShellEnvVars( electronPath: string, shellFullPath?: string, @@ -238,19 +197,3 @@ export function getDefShell(): Promise { }); }); } - -export function getShells(): Promise { - return new Promise((resolve, reject) => { - getProfiles() - .then((profiles: ITerminalProfile[]) => { - const shells: string[] = []; - profiles.forEach((p) => { - if (shells.indexOf(p.path) === -1) { - shells.push(p.path); - } - }); - resolve(shells); - }) - .catch(reject); - }); -} diff --git a/application/holder/src/env/os/powershell.ts b/application/holder/src/env/os/powershell.ts deleted file mode 100644 index c4779dda8..000000000 --- a/application/holder/src/env/os/powershell.ts +++ /dev/null @@ -1,328 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as os from 'os'; -import * as paths from 'path'; -import * as fs from 'fs'; -import { SymlinkSupport } from './symlink'; - -// This is required, since parseInt("7-preview") will return 7. -const IntRegex = /^\d+$/; - -const PwshMsixRegex = /^Microsoft.PowerShell_.*/; -const PwshPreviewMsixRegex = /^Microsoft.PowerShellPreview_.*/; - -const enum Arch { - x64, - x86, - ARM, -} - -let processArch: Arch; -switch (process.arch) { - case 'ia32': - case 'arm': - case 'arm64': - processArch = Arch.ARM; - break; - default: - processArch = Arch.x64; - break; -} - -/* -Currently, here are the values for these environment variables on their respective archs: - -On x86 process on x86: -PROCESSOR_ARCHITECTURE is X86 -PROCESSOR_ARCHITEW6432 is undefined - -On x86 process on x64: -PROCESSOR_ARCHITECTURE is X86 -PROCESSOR_ARCHITEW6432 is AMD64 - -On x64 process on x64: -PROCESSOR_ARCHITECTURE is AMD64 -PROCESSOR_ARCHITEW6432 is undefined - -On ARM process on ARM: -PROCESSOR_ARCHITECTURE is ARM64 -PROCESSOR_ARCHITEW6432 is undefined - -On x86 process on ARM: -PROCESSOR_ARCHITECTURE is X86 -PROCESSOR_ARCHITEW6432 is ARM64 - -On x64 process on ARM: -PROCESSOR_ARCHITECTURE is ARM64 -PROCESSOR_ARCHITEW6432 is undefined -*/ -let osArch: Arch; -if (process.env['PROCESSOR_ARCHITEW6432']) { - osArch = process.env['PROCESSOR_ARCHITEW6432'] === 'ARM64' ? Arch.ARM : Arch.x64; -} else if (process.env['PROCESSOR_ARCHITECTURE'] === 'ARM64') { - osArch = Arch.ARM; -} else if (process.env['PROCESSOR_ARCHITECTURE'] === 'X86') { - osArch = Arch.x86; -} else { - osArch = Arch.x64; -} - -export interface IPowerShellExeDetails { - readonly displayName: string; - readonly exePath: string; -} - -export interface IPossiblePowerShellExe extends IPowerShellExeDetails { - exists(): Promise; -} - -class PossiblePowerShellExe implements IPossiblePowerShellExe { - constructor( - public readonly exePath: string, - public readonly displayName: string, - private knownToExist?: boolean, - ) {} - - public async exists(): Promise { - if (this.knownToExist === undefined) { - this.knownToExist = await SymlinkSupport.existsFile(this.exePath); - } - return this.knownToExist; - } -} - -function getProgramFilesPath({ - useAlternateBitness = false, -}: { useAlternateBitness?: boolean } = {}): string | null { - if (!useAlternateBitness) { - // Just use the native system bitness - return process.env['ProgramFiles'] || null; - } - - // We might be a 64-bit process looking for 32-bit program files - if (processArch === Arch.x64) { - return process.env['ProgramFiles(x86)'] || null; - } - - // We might be a 32-bit process looking for 64-bit program files - if (osArch === Arch.x64) { - return process.env['ProgramW6432'] || null; - } - - // We're a 32-bit process on 32-bit Windows, there is no other Program Files dir - return null; -} - -async function findPSCoreWindowsInstallation({ - useAlternateBitness = false, - findPreview = false, -}: { - useAlternateBitness?: boolean; - findPreview?: boolean; -} = {}): Promise { - const programFilesPath = getProgramFilesPath({ useAlternateBitness }); - if (!programFilesPath) { - return null; - } - - const powerShellInstallBaseDir = paths.join(programFilesPath, 'PowerShell'); - - // Ensure the base directory exists - if (!(await SymlinkSupport.existsDirectory(powerShellInstallBaseDir))) { - return null; - } - - let highestSeenVersion = -1; - let pwshExePath: string | null = null; - for (const item of await fs.promises.readdir(powerShellInstallBaseDir)) { - let currentVersion = -1; - if (findPreview) { - // We are looking for something like "7-preview" - - // Preview dirs all have dashes in them - const dashIndex = item.indexOf('-'); - if (dashIndex < 0) { - continue; - } - - // Verify that the part before the dash is an integer - // and that the part after the dash is "preview" - const intPart: string = item.substring(0, dashIndex); - if (!IntRegex.test(intPart) || item.substring(dashIndex + 1) !== 'preview') { - continue; - } - - currentVersion = parseInt(intPart, 10); - } else { - // Search for a directory like "6" or "7" - if (!IntRegex.test(item)) { - continue; - } - - currentVersion = parseInt(item, 10); - } - - // Ensure we haven't already seen a higher version - if (currentVersion <= highestSeenVersion) { - continue; - } - - // Now look for the file - const exePath = paths.join(powerShellInstallBaseDir, item, 'pwsh.exe'); - if (!(await SymlinkSupport.existsFile(exePath))) { - continue; - } - - pwshExePath = exePath; - highestSeenVersion = currentVersion; - } - - if (!pwshExePath) { - return null; - } - - const bitness: string = programFilesPath.includes('x86') ? ' (x86)' : ''; - const preview: string = findPreview ? ' Preview' : ''; - - return new PossiblePowerShellExe(pwshExePath, `PowerShell${preview}${bitness}`, true); -} - -async function findPSCoreMsix({ - findPreview, -}: { findPreview?: boolean } = {}): Promise { - // We can't proceed if there's no LOCALAPPDATA path - if (!process.env['LOCALAPPDATA']) { - return null; - } - - // Find the base directory for MSIX application exe shortcuts - const msixAppDir = paths.join(process.env['LOCALAPPDATA'], 'Microsoft', 'WindowsApps'); - - if (!(await SymlinkSupport.existsDirectory(msixAppDir))) { - return null; - } - - // Define whether we're looking for the preview or the stable - const { pwshMsixDirRegex, pwshMsixName } = findPreview - ? { pwshMsixDirRegex: PwshPreviewMsixRegex, pwshMsixName: 'PowerShell Preview (Store)' } - : { pwshMsixDirRegex: PwshMsixRegex, pwshMsixName: 'PowerShell (Store)' }; - - // We should find only one such application, so return on the first one - for (const subdir of await fs.promises.readdir(msixAppDir)) { - if (pwshMsixDirRegex.test(subdir)) { - const pwshMsixPath = paths.join(msixAppDir, subdir, 'pwsh.exe'); - return new PossiblePowerShellExe(pwshMsixPath, pwshMsixName); - } - } - - // If we find nothing, return null - return null; -} - -function findPSCoreDotnetGlobalTool(): IPossiblePowerShellExe { - const dotnetGlobalToolExePath: string = paths.join( - os.homedir(), - '.dotnet', - 'tools', - 'pwsh.exe', - ); - - return new PossiblePowerShellExe(dotnetGlobalToolExePath, '.NET Core PowerShell Global Tool'); -} - -function findWinPS(): IPossiblePowerShellExe | null { - const winPSPath = paths.join( - process.env['windir']!, - processArch === Arch.x86 && osArch !== Arch.x86 ? 'SysNative' : 'System32', - 'WindowsPowerShell', - 'v1.0', - 'powershell.exe', - ); - - return new PossiblePowerShellExe(winPSPath, 'Windows PowerShell', true); -} - -/** - * Iterates through all the possible well-known PowerShell installations on a machine. - * Returned values may not exist, but come with an .exists property - * which will check whether the executable exists. - */ -async function* enumerateDefaultPowerShellInstallations(): AsyncIterable { - // Find PSCore stable first - let pwshExe = await findPSCoreWindowsInstallation(); - if (pwshExe) { - yield pwshExe; - } - - // Windows may have a 32-bit pwsh.exe - pwshExe = await findPSCoreWindowsInstallation({ useAlternateBitness: true }); - if (pwshExe) { - yield pwshExe; - } - - // Also look for the MSIX/UWP installation - pwshExe = await findPSCoreMsix(); - if (pwshExe) { - yield pwshExe; - } - - // Look for the .NET global tool - // Some older versions of PowerShell have a bug in this where startup will fail, - // but this is fixed in newer versions - pwshExe = findPSCoreDotnetGlobalTool(); - if (pwshExe) { - yield pwshExe; - } - - // Look for PSCore preview - pwshExe = await findPSCoreWindowsInstallation({ findPreview: true }); - if (pwshExe) { - yield pwshExe; - } - - // Find a preview MSIX - pwshExe = await findPSCoreMsix({ findPreview: true }); - if (pwshExe) { - yield pwshExe; - } - - // Look for pwsh-preview with the opposite bitness - pwshExe = await findPSCoreWindowsInstallation({ useAlternateBitness: true, findPreview: true }); - if (pwshExe) { - yield pwshExe; - } - - // Finally, get Windows PowerShell - pwshExe = findWinPS(); - if (pwshExe) { - yield pwshExe; - } -} - -/** - * Iterates through PowerShell installations on the machine according - * to configuration passed in through the constructor. - * PowerShell items returned by this object are verified - * to exist on the filesystem. - */ -export async function* enumeratePowerShellInstallations(): AsyncIterable { - // Get the default PowerShell installations first - for await (const defaultPwsh of enumerateDefaultPowerShellInstallations()) { - if (await defaultPwsh.exists()) { - yield defaultPwsh; - } - } -} - -/** - * Returns the first available PowerShell executable found in the search order. - */ -export async function getFirstAvailablePowerShellInstallation(): Promise { - for await (const pwsh of enumeratePowerShellInstallations()) { - return pwsh; - } - return null; -} diff --git a/application/holder/src/env/os/profiles.ts b/application/holder/src/env/os/profiles.ts deleted file mode 100644 index e54a96f3b..000000000 --- a/application/holder/src/env/os/profiles.ts +++ /dev/null @@ -1,392 +0,0 @@ -/* eslint-disable no-prototype-builtins */ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// NOTE: Chipmunk's partial copying of VSCode solution. Related modules/libs: -// https://github.com/microsoft/vscode/blob/main/src/vs/workbench/contrib/terminal/node/terminalProfiles.ts -// https://github.com/microsoft/vscode/blob/main/src/vs/workbench/contrib/terminal/node/terminalEnvironment.ts -// https://github.com/microsoft/vscode/blob/main/src/vs/base/node/pfs.ts - -import * as fs from 'fs'; -import * as os from 'os'; -import * as cp from 'child_process'; -import * as paths from 'path'; -import * as ps from './powershell'; - -const ENOENT = 'ENOENT'; - -export interface ITerminalProfile { - profileName: string; - path: string; - args?: string | string[] | undefined; - env?: ITerminalEnvironment; -} - -export interface ITerminalEnvironment { - [key: string]: string | null | undefined; -} - -export const enum ProfileSource { - GitBash = 'Git Bash', - Pwsh = 'PowerShell', -} - -export interface ITerminalExecutable { - path: string | string[]; - args?: string | string[] | undefined; - env?: ITerminalEnvironment; -} - -export interface ITerminalProfileSource { - source: ProfileSource; - args?: string | string[] | undefined; - env?: ITerminalEnvironment; -} - -export type ITerminalProfileObject = ITerminalExecutable | ITerminalProfileSource | null; - -export function exists(filename: string): Promise { - return new Promise((resolve, reject) => { - fs.access(filename, fs.constants.F_OK, (err: NodeJS.ErrnoException | null) => { - if (err) { - if (err.code === ENOENT) { - return resolve(false); - } else { - return reject(err); - } - } else { - resolve(true); - } - }); - }); -} - -let profileSources: Map | undefined; - -export function getWindowsBuildNumber(): number { - const osVersion = /(\d+)\.(\d+)\.(\d+)/g.exec(os.release()); - let buildNumber = 0; - if (osVersion && osVersion.length === 4) { - buildNumber = parseInt(osVersion[3]); - } - return buildNumber; -} - -export function getCaseInsensitive(target: any, key: string): any { - const lowercaseKey = key.toLowerCase(); - const equivalentKey = Object.keys(target).find((k) => k.toLowerCase() === lowercaseKey); - return equivalentKey ? target[equivalentKey] : target[key]; -} - -export async function findExecutable( - command: string, - cwd?: string, - pathsToCheck?: string[], - env: ITerminalEnvironment = process.env as ITerminalEnvironment, -): Promise { - // If we have an absolute path then we take it. - if (paths.isAbsolute(command)) { - return (await exists(command)) ? command : undefined; - } - if (cwd === undefined) { - cwd = process.cwd(); - } - const dir = paths.dirname(command); - if (dir !== '.') { - // We have a directory and the directory is relative (see above). Make the path absolute - // to the current working directory. - const fullPath = paths.join(cwd, command); - return (await exists(fullPath)) ? fullPath : undefined; - } - const envPath = getCaseInsensitive(env, 'PATH'); - if (pathsToCheck === undefined && typeof envPath === 'string') { - pathsToCheck = envPath.split(paths.delimiter); - } - // No PATH environment. Make path absolute to the cwd. - if (pathsToCheck === undefined || pathsToCheck.length === 0) { - const fullPath = paths.join(cwd, command); - return (await exists(fullPath)) ? fullPath : undefined; - } - // We have a simple file name. We get the path variable from the env - // and try to find the executable on the path. - for (const pathEntry of pathsToCheck) { - // The path entry is absolute. - let fullPath: string; - if (paths.isAbsolute(pathEntry)) { - fullPath = paths.join(pathEntry, command); - } else { - fullPath = paths.join(cwd, pathEntry, command); - } - - if (await exists(fullPath)) { - return fullPath; - } - if (process.platform === 'win32') { - let withExtension = fullPath + '.com'; - if (await exists(withExtension)) { - return withExtension; - } - withExtension = fullPath + '.exe'; - if (await exists(withExtension)) { - return withExtension; - } - } - } - const fullPath = paths.join(cwd, command); - return (await exists(fullPath)) ? fullPath : undefined; -} - -export function detectAvailableProfiles(): Promise { - if (process.platform === 'win32') { - return detectAvailableWindowsProfiles(); - } - return detectAvailableUnixProfiles(); -} - -async function detectAvailableWindowsProfiles(): Promise { - // Determine the correct System32 path. We want to point to Sysnative - // when the 32-bit version of VS Code is running on a 64-bit machine. - // The reason for this is because PowerShell's important PSReadline - // module doesn't work if this is not the case. See #27915. - const is32ProcessOn64Windows = process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'); - const system32Path = `${process.env['windir']}\\${ - is32ProcessOn64Windows ? 'Sysnative' : 'System32' - }`; - - let useWSLexe = false; - - if (getWindowsBuildNumber() >= 16299) { - useWSLexe = true; - } - - await initializeWindowsProfiles(); - - const detectedProfiles: Map = new Map(); - - // Add auto detected profiles - detectedProfiles.set('PowerShell', { - source: ProfileSource.Pwsh, - }); - detectedProfiles.set('Windows PowerShell', { - path: `${system32Path}\\WindowsPowerShell\\v1.0\\powershell.exe`, - }); - detectedProfiles.set('Git Bash', { source: ProfileSource.GitBash }); - detectedProfiles.set('Cygwin', { - path: [ - `${process.env['HOMEDRIVE']}\\cygwin64\\bin\\bash.exe`, - `${process.env['HOMEDRIVE']}\\cygwin\\bin\\bash.exe`, - ], - args: ['--login'], - }); - detectedProfiles.set('Command Prompt', { - path: `${system32Path}\\cmd.exe`, - }); - - const resultProfiles: ITerminalProfile[] = await transformToTerminalProfiles( - detectedProfiles.entries(), - ); - - try { - const result = await getWslProfiles( - `${system32Path}\\${useWSLexe ? 'wsl.exe' : 'bash.exe'}`, - useWSLexe, - ); - if (result) { - resultProfiles.push(...result); - } - } catch (_err) { - console.info('WSL is not installed, so could not detect WSL profiles'); - } - - return resultProfiles; -} - -async function transformToTerminalProfiles( - entries: IterableIterator<[string, ITerminalProfileObject]>, -): Promise { - const resultProfiles: ITerminalProfile[] = []; - for (const [profileName, profile] of entries) { - if (profile === null) { - continue; - } - let originalPaths: string[]; - let args: string[] | string | undefined; - if ('source' in profile) { - const source = profileSources?.get(profile.source); - if (!source) { - continue; - } - originalPaths = source.paths; - - // if there are configured args, override the default ones - args = profile.args || source.args; - } else { - originalPaths = Array.isArray(profile.path) ? profile.path : [profile.path]; - args = - process.platform === 'win32' - ? profile.args - : Array.isArray(profile.args) - ? profile.args - : undefined; - } - - const paths = originalPaths.slice(); - - const validatedProfile = await validateProfilePaths(profileName, paths, args, profile.env); - if (validatedProfile) { - resultProfiles.push(validatedProfile); - } - } - return resultProfiles; -} - -async function initializeWindowsProfiles(): Promise { - if (profileSources) { - return; - } - - profileSources = new Map(); - profileSources.set('Git Bash', { - profileName: 'Git Bash', - paths: [ - `${process.env['ProgramW6432']}\\Git\\bin\\bash.exe`, - `${process.env['ProgramW6432']}\\Git\\usr\\bin\\bash.exe`, - `${process.env['ProgramFiles']}\\Git\\bin\\bash.exe`, - `${process.env['ProgramFiles']}\\Git\\usr\\bin\\bash.exe`, - `${process.env['LocalAppData']}\\Programs\\Git\\bin\\bash.exe`, - ], - args: ['--login'], - }); - profileSources.set('PowerShell', { - profileName: 'PowerShell', - paths: await getPowershellPaths(), - }); -} - -async function getPowershellPaths(): Promise { - const paths: string[] = []; - // Add all of the different kinds of PowerShells - for await (const pwshExe of ps.enumeratePowerShellInstallations()) { - paths.push(pwshExe.exePath); - } - return paths; -} - -async function getWslProfiles( - wslPath: string, - useWslProfiles?: boolean, -): Promise { - const profiles: ITerminalProfile[] = []; - if (useWslProfiles) { - const distroOutput = await new Promise((resolve, reject) => { - // wsl.exe output is encoded in utf16le (ie. A -> 0x4100) - cp.exec('wsl.exe -l -q', { encoding: 'utf16le' }, (err, stdout) => { - if (err) { - return reject(new Error('Problem occurred when getting wsl distros')); - } - resolve(stdout); - }); - }); - if (distroOutput) { - const regex = new RegExp(/[\r?\n]/); - const distroNames = distroOutput - .split(regex) - .filter((t) => t.trim().length > 0 && t !== ''); - for (const distroName of distroNames) { - // Skip empty lines - if (distroName === '') { - continue; - } - - // docker-desktop and docker-desktop-data are treated as implementation details of - // Docker Desktop for Windows and therefore not exposed - if (distroName.startsWith('docker-desktop')) { - continue; - } - const profile: ITerminalProfile = { - profileName: `${distroName} (WSL)`, - path: wslPath, - args: [`-d`, `${distroName}`], - }; - // Add the profile - profiles.push(profile); - } - return profiles; - } - } - return []; -} - -async function detectAvailableUnixProfiles(): Promise { - const detectedProfiles: Map = new Map(); - - const contents = await fs.promises.readFile('/etc/shells', 'utf8'); - const profiles = contents - .split('\n') - .filter((e) => e.trim().indexOf('#') !== 0 && e.trim().length > 0); - const counts: Map = new Map(); - for (const profile of profiles) { - let profileName = paths.basename(profile); - let count = counts.get(profileName) || 0; - count++; - if (count > 1) { - profileName = `${profileName} (${count})`; - } - counts.set(profileName, count); - detectedProfiles.set(profileName, { path: profile }); - } - return await transformToTerminalProfiles(detectedProfiles.entries()); -} - -async function validateProfilePaths( - profileName: string, - potentialPaths: string[], - args?: string[] | string, - env?: ITerminalEnvironment, -): Promise { - if (potentialPaths.length === 0) { - return Promise.resolve(undefined); - } - const path = potentialPaths.shift()!; - if (path === '') { - return validateProfilePaths(profileName, potentialPaths, args, env); - } - - const profile: ITerminalProfile = { profileName, path, args, env }; - - // For non-absolute paths, check if it's available on $PATH - if (paths.basename(path) === path) { - // The executable isn't an absolute path, try find it on the PATH - const envPaths: string[] | undefined = process.env['PATH'] - ? process.env['PATH'].split(paths.delimiter) - : undefined; - const executable = await findExecutable(path, undefined, envPaths, undefined); - if (!executable) { - return validateProfilePaths(profileName, potentialPaths, args); - } - return profile; - } - - const result = await exists(paths.normalize(path)); - if (result) { - return profile; - } - - return validateProfilePaths(profileName, potentialPaths, args, env); -} - -export interface IFsProvider { - existsFile(path: string): Promise; - readFile( - path: string, - options: { encoding: BufferEncoding; flag?: string | number } | BufferEncoding, - ): Promise; -} - -interface IPotentialTerminalProfile { - profileName: string; - paths: string[]; - args?: string[]; -} diff --git a/application/holder/src/env/os/symlink.ts b/application/holder/src/env/os/symlink.ts deleted file mode 100644 index 65c03751e..000000000 --- a/application/holder/src/env/os/symlink.ts +++ /dev/null @@ -1,125 +0,0 @@ -/* eslint-disable @typescript-eslint/no-namespace */ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; - -export namespace SymlinkSupport { - export interface IStats { - // The stats of the file. If the file is a symbolic - // link, the stats will be of that target file and - // not the link itself. - // If the file is a symbolic link pointing to a non - // existing file, the stat will be of the link and - // the `dangling` flag will indicate this. - stat: fs.Stats; - - // Will be provided if the resource is a symbolic link - // on disk. Use the `dangling` flag to find out if it - // points to a resource that does not exist on disk. - symbolicLink?: { dangling: boolean }; - } - - /** - * Resolves the `fs.Stats` of the provided path. If the path is a - * symbolic link, the `fs.Stats` will be from the target it points - * to. If the target does not exist, `dangling: true` will be returned - * as `symbolicLink` value. - */ - export async function stat(path: string): Promise { - // First stat the link - let lstats: fs.Stats | undefined; - try { - lstats = await fs.promises.lstat(path); - - // Return early if the stat is not a symbolic link at all - if (!lstats.isSymbolicLink()) { - return { stat: lstats }; - } - } catch (_err) { - /* ignore - use stat() instead */ - } - - // If the stat is a symbolic link or failed to stat, use fs.stat() - // which for symbolic links will stat the target they point to - try { - const stats = await fs.promises.stat(path); - - return { - stat: stats, - symbolicLink: lstats?.isSymbolicLink() ? { dangling: false } : undefined, - }; - } catch (error) { - // If the link points to a non-existing file we still want - // to return it as result while setting dangling: true flag - if ((error as any).code === 'ENOENT' && lstats) { - return { stat: lstats, symbolicLink: { dangling: true } }; - } - - // Windows: workaround a node.js bug where reparse points - // are not supported (https://github.com/nodejs/node/issues/36790) - if (process.platform === 'win32' && (error as any).code === 'EACCES') { - try { - const stats = await fs.promises.stat(await fs.promises.readlink(path)); - - return { stat: stats, symbolicLink: { dangling: false } }; - } catch (error) { - // If the link points to a non-existing file we still want - // to return it as result while setting dangling: true flag - if ((error as any).code === 'ENOENT' && lstats) { - return { stat: lstats, symbolicLink: { dangling: true } }; - } - - throw error; - } - } - - throw error; - } - } - - /** - * Figures out if the `path` exists and is a file with support - * for symlinks. - * - * Note: this will return `false` for a symlink that exists on - * disk but is dangling (pointing to a non-existing path). - * - * Use `exists` if you only care about the path existing on disk - * or not without support for symbolic links. - */ - export async function existsFile(path: string): Promise { - try { - const { stat, symbolicLink } = await SymlinkSupport.stat(path); - - return stat.isFile() && symbolicLink?.dangling !== true; - } catch (_err) { - // Ignore, path might not exist - } - - return false; - } - - /** - * Figures out if the `path` exists and is a directory with support for - * symlinks. - * - * Note: this will return `false` for a symlink that exists on - * disk but is dangling (pointing to a non-existing path). - * - * Use `exists` if you only care about the path existing on disk - * or not without support for symbolic links. - */ - export async function existsDirectory(path: string): Promise { - try { - const { stat, symbolicLink } = await SymlinkSupport.stat(path); - - return stat.isDirectory() && symbolicLink?.dangling !== true; - } catch (_err) { - // Ignore, path might not exist - } - - return false; - } -}