|
| 1 | +// Copyright (c) Microsoft Corporation. All rights reserved. |
| 2 | +// Licensed under the MIT License. |
| 3 | + |
| 4 | +'use strict'; |
| 5 | + |
| 6 | +import * as path from 'path'; |
| 7 | +import { dirname } from 'path'; |
| 8 | +import { |
| 9 | + arePathsSame, |
| 10 | + getPythonSetting, |
| 11 | + onDidChangePythonSetting, |
| 12 | + pathExists, |
| 13 | + shellExecute, |
| 14 | +} from '../externalDependencies'; |
| 15 | +import { cache } from '../../../common/utils/decorators'; |
| 16 | +import { traceError, traceVerbose } from '../../../logging'; |
| 17 | +import { getOSType, getUserHomeDir, OSType } from '../../../common/utils/platform'; |
| 18 | + |
| 19 | +export const ACTIVESTATETOOLPATH_SETTING_KEY = 'activeStateToolPath'; |
| 20 | + |
| 21 | +const STATE_GENERAL_TIMEOUT = 5000; |
| 22 | + |
| 23 | +export type ProjectInfo = { |
| 24 | + name: string; |
| 25 | + organization: string; |
| 26 | + local_checkouts: string[]; // eslint-disable-line camelcase |
| 27 | + executables: string[]; |
| 28 | +}; |
| 29 | + |
| 30 | +export async function isActiveStateEnvironment(interpreterPath: string): Promise<boolean> { |
| 31 | + const execDir = path.dirname(interpreterPath); |
| 32 | + const runtimeDir = path.dirname(execDir); |
| 33 | + return pathExists(path.join(runtimeDir, '_runtime_store')); |
| 34 | +} |
| 35 | + |
| 36 | +export class ActiveState { |
| 37 | + private static statePromise: Promise<ActiveState | undefined> | undefined; |
| 38 | + |
| 39 | + public static async getState(): Promise<ActiveState | undefined> { |
| 40 | + if (ActiveState.statePromise === undefined) { |
| 41 | + ActiveState.statePromise = ActiveState.locate(); |
| 42 | + } |
| 43 | + return ActiveState.statePromise; |
| 44 | + } |
| 45 | + |
| 46 | + constructor() { |
| 47 | + onDidChangePythonSetting(ACTIVESTATETOOLPATH_SETTING_KEY, () => { |
| 48 | + ActiveState.statePromise = undefined; |
| 49 | + }); |
| 50 | + } |
| 51 | + |
| 52 | + public static getStateToolDir(): string | undefined { |
| 53 | + const home = getUserHomeDir(); |
| 54 | + if (!home) { |
| 55 | + return undefined; |
| 56 | + } |
| 57 | + return getOSType() === OSType.Windows |
| 58 | + ? path.join(home, 'AppData', 'Local', 'ActiveState', 'StateTool') |
| 59 | + : path.join(home, '.local', 'ActiveState', 'StateTool'); |
| 60 | + } |
| 61 | + |
| 62 | + private static async locate(): Promise<ActiveState | undefined> { |
| 63 | + const stateToolDir = this.getStateToolDir(); |
| 64 | + const stateCommand = |
| 65 | + getPythonSetting<string>(ACTIVESTATETOOLPATH_SETTING_KEY) ?? ActiveState.defaultStateCommand; |
| 66 | + if (stateToolDir && ((await pathExists(stateToolDir)) || stateCommand !== this.defaultStateCommand)) { |
| 67 | + return new ActiveState(); |
| 68 | + } |
| 69 | + return undefined; |
| 70 | + } |
| 71 | + |
| 72 | + public async getProjects(): Promise<ProjectInfo[] | undefined> { |
| 73 | + return this.getProjectsCached(); |
| 74 | + } |
| 75 | + |
| 76 | + private static readonly defaultStateCommand: string = 'state'; |
| 77 | + |
| 78 | + @cache(30_000, true, 10_000) |
| 79 | + // eslint-disable-next-line class-methods-use-this |
| 80 | + private async getProjectsCached(): Promise<ProjectInfo[] | undefined> { |
| 81 | + try { |
| 82 | + const stateCommand = |
| 83 | + getPythonSetting<string>(ACTIVESTATETOOLPATH_SETTING_KEY) ?? ActiveState.defaultStateCommand; |
| 84 | + const result = await shellExecute(`${stateCommand} projects -o editor`, { |
| 85 | + timeout: STATE_GENERAL_TIMEOUT, |
| 86 | + }); |
| 87 | + if (!result) { |
| 88 | + return undefined; |
| 89 | + } |
| 90 | + let output = result.stdout.trimEnd(); |
| 91 | + if (output[output.length - 1] === '\0') { |
| 92 | + // '\0' is a record separator. |
| 93 | + output = output.substring(0, output.length - 1); |
| 94 | + } |
| 95 | + traceVerbose(`${stateCommand} projects -o editor: ${output}`); |
| 96 | + const projects = JSON.parse(output); |
| 97 | + ActiveState.setCachedProjectInfo(projects); |
| 98 | + return projects; |
| 99 | + } catch (ex) { |
| 100 | + traceError(ex); |
| 101 | + return undefined; |
| 102 | + } |
| 103 | + } |
| 104 | + |
| 105 | + // Stored copy of known projects. isActiveStateEnvironmentForWorkspace() is |
| 106 | + // not async, so getProjects() cannot be used. ActiveStateLocator sets this |
| 107 | + // when it resolves project info. |
| 108 | + private static cachedProjectInfo: ProjectInfo[] = []; |
| 109 | + |
| 110 | + public static getCachedProjectInfo(): ProjectInfo[] { |
| 111 | + return this.cachedProjectInfo; |
| 112 | + } |
| 113 | + |
| 114 | + private static setCachedProjectInfo(projects: ProjectInfo[]): void { |
| 115 | + this.cachedProjectInfo = projects; |
| 116 | + } |
| 117 | +} |
| 118 | + |
| 119 | +export function isActiveStateEnvironmentForWorkspace(interpreterPath: string, workspacePath: string): boolean { |
| 120 | + const interpreterDir = dirname(interpreterPath); |
| 121 | + for (const project of ActiveState.getCachedProjectInfo()) { |
| 122 | + if (project.executables) { |
| 123 | + for (const [i, dir] of project.executables.entries()) { |
| 124 | + // Note multiple checkouts for the same interpreter may exist. |
| 125 | + // Check them all. |
| 126 | + if (arePathsSame(dir, interpreterDir) && arePathsSame(workspacePath, project.local_checkouts[i])) { |
| 127 | + return true; |
| 128 | + } |
| 129 | + } |
| 130 | + } |
| 131 | + } |
| 132 | + return false; |
| 133 | +} |
0 commit comments