reasonDesc: getTelemetryReasonDesc()

in packages/core/src/shared/fs/fs.ts [254:568]


                        reasonDesc: getTelemetryReasonDesc(e),
                    })
                    getLogger().warn(`writeFile atomic Node failed for, ${uri.fsPath}, with %O`, e)
                    // The atomic rename techniques were not successful, so we will
                    // just resort to regular a non-atomic write
                }
            } finally {
                // clean up temp file since it possibly remains
                if (await fs.exists(tempFile)) {
                    await fs.delete(tempFile)
                }
            }
        }

        await write(uri)
    }

    /**
     * Rename (move) a file or folder.
     * @param oldPath
     * @param newPath
     * @returns
     */
    async rename(oldPath: vscode.Uri | string, newPath: vscode.Uri | string) {
        const oldUri = toUri(oldPath)
        const newUri = toUri(newPath)
        const errHandler = createPermissionsErrorHandler(this.isWeb, oldUri, 'rw*')

        /**
         * We were seeing 'FileNotFound' errors during renames, even though we did a `writeFile()` right before the rename.
         * The error looks to be from here: https://github.com/microsoft/vscode/blob/09d5f4efc5089ce2fc5c8f6aeb51d728d7f4e758/src/vs/platform/files/node/diskFileSystemProvider.ts#L747
         * So a guess is that the exists()(stat() under the hood) call needs to be retried since there may be a race condition.
         */
        let attempts = 0
        const isExists = await waitUntil(async () => {
            const result = await fs.exists(oldUri)
            attempts += 1
            return result
        }, FileSystem.renameTimeoutOpts)
        // TODO: Move the `ide_fileSystem` or some variation of this metric in to common telemetry
        // TODO: Deduplicate the `ide_fileSystem` call. Maybe have a method that all operations pass through which wraps the call in telemetry.
        //       Then look to report telemetry failure events.
        const scrubbedPath = scrubNames(oldUri.fsPath)
        if (!isExists) {
            // source path never existed after multiple attempts.
            // Either the file never existed, or had we waited longer it would have. We won't know.
            telemetry.ide_fileSystem.emit({
                result: 'Failed',
                action: 'rename',
                reason: 'SourceNotExists',
                reasonDesc: `After ${FileSystem.renameTimeoutOpts.timeout}ms the source path did not exist: ${scrubbedPath}`,
            })
        } else if (attempts > 1) {
            // Indicates that rename() would have failed if we had not waited for it to exist.
            telemetry.ide_fileSystem.emit({
                result: 'Succeeded',
                action: 'rename',
                reason: 'RenameRaceCondition',
                reasonDesc: `After multiple attempts the source path existed: ${scrubbedPath}`,
                attempts: attempts,
            })
        }

        return vfs.rename(oldUri, newUri, { overwrite: true }).then(undefined, errHandler)
    }

    /**
     * It looks like scenario of a failed rename is rare,
     * so we can afford to have a longer timeout and interval.
     *
     * These values are an arbitrary guess to how long it takes
     * for a newly created file to be visible on the filesystem.
     */
    static readonly renameTimeoutOpts = {
        timeout: 10_000,
        interval: 300,
    } as const

    /**
     * The stat of the file,  throws if the file does not exist or on any other error.
     */
    async stat(uri: vscode.Uri | string): Promise<vscode.FileStat> {
        const path = toUri(uri)
        return await vfs.stat(path)
    }

    /**
     * Change permissions on file. Note that this will do nothing on browser.
     * @param uri file whose permissions should be set.
     * @param mode new permissions in octal notation.
     * More info: https://nodejs.org/api/fs.html#fspromiseschmodpath-mode
     * Examples: https://www.geeksforgeeks.org/node-js-fspromises-chmod-method/
     */
    async chmod(uri: vscode.Uri | string, mode: number): Promise<void> {
        if (!this.isWeb) {
            const path = toUri(uri)
            await chmod(path.fsPath, mode)
        }
    }

    /**
     * Deletes a file or directory. It is not an error if the file/directory does not exist, unless
     * its parent directory is not listable (executable).
     *
     * @param fileOrDir Path to file or directory
     * @param opt Options.
     * - `recursive`: forcefully delete a directory. Use `recursive:false` (the default) to prevent
     *   accidentally deleting a directory when a file is expected.
     * - `force`: ignore "not found" errors. Defaults to true if `recursive:true`, else defaults to
     *   false.
     */
    async delete(fileOrDir: string | vscode.Uri, opt_?: { recursive?: boolean; force?: boolean }): Promise<void> {
        const opt = { ...opt_, recursive: !!opt_?.recursive }
        opt.force = opt.force === false ? opt.force : !!(opt.force || opt.recursive)
        const uri = toUri(fileOrDir)
        const parent = vscode.Uri.joinPath(uri, '..')
        const errHandler = createPermissionsErrorHandler(this.isWeb, parent, '*wx')

        if (opt.recursive) {
            // Error messages may be misleading if using the `recursive` option.
            // Need to implement our own recursive delete if we want detailed info.
            return vfs.delete(uri, opt).then(undefined, (err) => {
                if (!opt.force || !isFileNotFoundError(err)) {
                    throw err
                }
                // Else: ignore "not found" error.
            })
        }

        return vfs.delete(uri, opt).then(undefined, async (err) => {
            const notFound = isFileNotFoundError(err)

            if (notFound && opt.force) {
                return // Ignore "not found" error.
            } else if (this.isWeb || isPermissionsError(err)) {
                throw await errHandler(err)
            } else if (uri.scheme !== 'file' || (!isWin() && !notFound)) {
                throw err
            } else {
                // Try to build a more detailed "not found" error.

                // if (isMinVscode('1.80.0') && notFound) {
                //     return // Old Nodejs does not have constants.S_IXUSR.
                // }

                // Attempting to delete a file in a non-executable directory results in ENOENT.
                // But this might not be true. The file could exist, we just don't know about it.
                // Note: Windows has no "x" (executable) flag.
                const parentStat = await nodefs.stat(parent.fsPath).catch(() => {
                    throw err
                })
                const isParentExecutable = isWin() || !!(parentStat.mode & nodeConstants.S_IXUSR)
                if (!isParentExecutable) {
                    const userInfo = this.getUserInfo()
                    throw new PermissionsError(parent, parentStat, userInfo, '*wx', err)
                } else if (notFound) {
                    return
                }
            }

            throw err
        })
    }

    async readdir(uri: vscode.Uri | string): Promise<[string, vscode.FileType][]> {
        return await vfs.readDirectory(toUri(uri))
    }

    /**
     * Copy target file or directory
     * @param source
     * @param target
     * @returns
     */
    async copy(source: vscode.Uri | string, target: vscode.Uri | string): Promise<void> {
        const sourcePath = toUri(source)
        const targetPath = toUri(target)
        return await vfs.copy(sourcePath, targetPath, { overwrite: true })
    }

    /**
     * Checks if the current user has _at least_ the specified permissions.
     *
     * This throws {@link PermissionsError} when permissions are insufficient.
     */
    async checkPerms(file: string | vscode.Uri, perms: PermissionsTriplet): Promise<void> {
        // TODO: implement checkExactPerms() by checking the file mode.
        // public static async checkExactPerms(file: string | vscode.Uri, perms: `${PermissionsTriplet}${PermissionsTriplet}${PermissionsTriplet}`)
        const uri = toUri(file)
        const errHandler = createPermissionsErrorHandler(this.isWeb, uri, perms)
        const flags = Array.from(perms) as (keyof typeof this.modeMap)[]
        const mode = flags.reduce((m, f) => m | this.modeMap[f], nodeConstants.F_OK)

        return nodefs.access(uri.fsPath, mode).catch(errHandler)
    }

    /**
     * Returns the file or directory location given by `envVar` if it is non-empty and the location is valid (exists) on the filesystem.
     *
     * Special case: if 'HOMEPATH' is given then $HOMEDRIVE is prepended to it.
     *
     * Throws an exception if the env var path is non-empty but invalid.
     *
     * @param envVar Environment variable name
     * @param kind Expect a valid file, directory, or either.
     */
    async tryGetFilepathEnvVar(envVar: string, kind: vscode.FileType | undefined): Promise<string | undefined> {
        let envVal = process.env[envVar]

        if (envVal) {
            // Special case: Windows $HOMEPATH depends on $HOMEDRIVE.
            if (envVar === 'HOMEPATH') {
                const homeDrive = process.env.HOMEDRIVE || 'C:'
                envVal = _path.join(homeDrive, envVal)
            }

            // Expand "~/" to home dir.
            const f = resolvePath(envVal, this.#homeDir ?? 'UNKNOWN-HOME')
            if (await fs.exists(f, kind)) {
                return f
            }

            throw new ToolkitError(`\$${envVar} filepath is invalid: "${f}"`)
        }
    }

    /**
     * Initializes the FileSystem object. Resolves the user's home directory and validates related
     * environment variables. Should be called:
     *  1. at startup after all env vars are set
     *  2. whenver env vars change
     *
     * @param onFail Invoked if a valid home directory could not be resolved
     * @returns List of error messages if any invalid env vars were found.
     */
    async init(
        extContext: vscode.ExtensionContext,
        onFail: (homeDir: string) => void,
        osUserInfo?: typeof os.userInfo
    ): Promise<string[]> {
        this.#username = undefined
        this.#osUserInfo = osUserInfo ?? os.userInfo

        if (this.isWeb) {
            // When in browser we cannot access the users desktop file system.
            // Instead, VS Code provided uris will use the browsers storage.
            // IMPORTANT: we must preserve the scheme of this URI or else VS Code
            // will incorrectly interpret the path.
            this.#homeDir = extContext.globalStorageUri.toString()
            return []
        }

        /** Logger may not be available during startup, so messages are stored here. */
        const logMsgs: string[] = []
        function logErr(e: unknown): undefined {
            logMsgs.push((e as Error).message)
            return undefined
        }
        const tryGet = (envName: string) => {
            return this.tryGetFilepathEnvVar(envName, vscode.FileType.Directory).catch(logErr)
        }
        let p: string | undefined
        if ((p = await tryGet('HOME'))) {
            this.#homeDir = p
        } else if ((p = await tryGet('USERPROFILE'))) {
            this.#homeDir = p
        } else if ((p = await tryGet('HOMEPATH'))) {
            this.#homeDir = p
        } else {
            this.#homeDir = os.homedir()
        }

        // If $HOME is bogus, os.homedir() will still return it! All we can do is show an error.
        if (!(await fs.exists(this.#homeDir, vscode.FileType.Directory))) {
            onFail(this.#homeDir)
        }

        return logMsgs
    }

    /**
     * Gets the (cached) user home directory path.
     *
     * To update the cached value (e.g. after environment variables changed), call {@link init()}.
     */
    getUserHomeDir(): string {
        if (!this.#homeDir) {
            throw new Error('call fs.init() before using fs.getHomeDirectory()')
        }
        return this.#homeDir
    }

    /**
     * Gets the application cache folder for the current platform
     *
     * Follows the cache_dir convention outlined in https://crates.io/crates/dirs
     */
    getCacheDir(): string {
        if (isWeb()) {
            const homeDir = this.#homeDir
            if (!homeDir) {
                throw new ToolkitError('Web home directory not found', {
                    code: 'WebHomeDirectoryNotFound',
                })
            }
            return homeDir
        }
        switch (process.platform) {
            case 'darwin': {
                return _path.join(this.getUserHomeDir(), 'Library/Caches')
            }
            case 'win32': {
                const localAppData = process.env.LOCALAPPDATA
                if (!localAppData) {
                    throw new ToolkitError('LOCALAPPDATA environment variable not set', {