Skip to content

Commit 4006afe

Browse files
committed
feat: add support for cmd activate
1 parent d90d69f commit 4006afe

1 file changed

Lines changed: 311 additions & 0 deletions

File tree

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
import * as fs from 'fs-extra';
2+
import * as path from 'path';
3+
import * as os from 'os';
4+
import { isWindows } from '../../../common/utils/platformUtils';
5+
import { ShellScriptEditState, ShellSetupState, ShellStartupProvider } from './startupProvider';
6+
import { EnvironmentVariableCollection } from 'vscode';
7+
import { PythonEnvironment, TerminalShellType } from '../../../api';
8+
import { getActivationCommandForShell } from '../../common/activation';
9+
import { traceError, traceInfo, traceVerbose } from '../../../common/logging';
10+
import { getCommandAsString } from './utils';
11+
import which from 'which';
12+
import * as cp from 'child_process';
13+
import { promisify } from 'util';
14+
15+
const exec = promisify(cp.exec);
16+
17+
async function isCmdInstalled(): Promise<boolean> {
18+
if (!isWindows()) {
19+
return false;
20+
}
21+
22+
if (process.env.ComSpec && (await fs.exists(process.env.ComSpec))) {
23+
return true;
24+
}
25+
26+
try {
27+
// Try to find cmd.exe on the system
28+
await which('cmd.exe', { nothrow: true });
29+
return true;
30+
} catch {
31+
// This should normally not happen on Windows
32+
return false;
33+
}
34+
}
35+
36+
async function getCmdFilePaths(): Promise<{ startupFile: string; mainBatchFile: string }> {
37+
const homeDir = os.homedir();
38+
return {
39+
mainBatchFile: path.join(homeDir, 'cmd_startup.bat'),
40+
startupFile: path.join(homeDir, 'vscode-python-cmd-init.cmd'),
41+
};
42+
}
43+
44+
const regionStart = 'rem >>> vscode python';
45+
const regionEnd = 'rem <<< vscode python';
46+
47+
function getActivationContent(key: string): string {
48+
const lineSep = isWindows() ? '\r\n' : '\n';
49+
return ['', '', regionStart, `if defined ${key} (`, ` %${key}%`, ')', regionEnd, ''].join(lineSep);
50+
}
51+
52+
function getMainBatchFileContent(startupFile: string, existingContent?: string): string {
53+
const lineSep = isWindows() ? '\r\n' : '\n';
54+
let content = [];
55+
56+
// Add header
57+
content.push('@echo off');
58+
content.push('rem This file is managed by VS Code Python extension');
59+
content.push('');
60+
61+
// Add existing AutoRun content if any
62+
if (existingContent && existingContent.trim()) {
63+
content.push('rem Original AutoRun content');
64+
content.push(existingContent);
65+
content.push('');
66+
}
67+
68+
// Add our startup file call
69+
content.push('rem VS Code Python environment activation');
70+
content.push(`if exist "${startupFile}" call "${startupFile}"`);
71+
72+
return content.join(lineSep);
73+
}
74+
75+
async function checkRegistryAutoRun(mainBatchFile: string): Promise<boolean> {
76+
if (!isWindows()) {
77+
return false;
78+
}
79+
80+
try {
81+
// Check if AutoRun is set in the registry to call our batch file
82+
const { stdout } = await exec('reg query "HKCU\\Software\\Microsoft\\Command Processor" /v AutoRun', {
83+
windowsHide: true,
84+
});
85+
86+
// Check if the output contains our batch file path
87+
return stdout.includes(mainBatchFile);
88+
} catch {
89+
// If the command fails, the registry key might not exist
90+
return false;
91+
}
92+
}
93+
94+
async function getExistingAutoRun(): Promise<string | undefined> {
95+
if (!isWindows()) {
96+
return undefined;
97+
}
98+
99+
try {
100+
const { stdout } = await exec('reg query "HKCU\\Software\\Microsoft\\Command Processor" /v AutoRun', {
101+
windowsHide: true,
102+
});
103+
104+
const match = stdout.match(/AutoRun\s+REG_SZ\s+(.*)/);
105+
if (match && match[1]) {
106+
const content = match[1].trim();
107+
// Don't return our own batch file calls
108+
if (content.includes('cmd_startup.bat')) {
109+
return undefined;
110+
}
111+
return content;
112+
}
113+
} catch {
114+
// Key doesn't exist yet
115+
}
116+
117+
return undefined;
118+
}
119+
120+
async function setupRegistryAutoRun(mainBatchFile: string): Promise<boolean> {
121+
if (!isWindows()) {
122+
return false;
123+
}
124+
125+
try {
126+
// Set the registry key to call our main batch file
127+
await exec(
128+
`reg add "HKCU\\Software\\Microsoft\\Command Processor" /v AutoRun /t REG_SZ /d "call \\"${mainBatchFile}\\"" /f`,
129+
{ windowsHide: true },
130+
);
131+
132+
traceInfo(`Set CMD AutoRun registry key to call: ${mainBatchFile}`);
133+
return true;
134+
} catch (err) {
135+
traceError('Failed to set CMD AutoRun registry key', err);
136+
return false;
137+
}
138+
}
139+
140+
async function isCmdStartupSetup(startupFile: string, mainBatchFile: string, key: string): Promise<boolean> {
141+
// Check both the startup file and registry AutoRun setting
142+
const fileExists = await fs.pathExists(startupFile);
143+
const fileHasContent = fileExists ? (await fs.readFile(startupFile, 'utf8')).includes(key) : false;
144+
145+
const mainFileExists = await fs.pathExists(mainBatchFile);
146+
const registrySetup = await checkRegistryAutoRun(mainBatchFile);
147+
148+
return fileHasContent && mainFileExists && registrySetup;
149+
}
150+
151+
async function setupCmdStartup(startupFile: string, mainBatchFile: string, key: string): Promise<boolean> {
152+
try {
153+
const activationContent = getActivationContent(key);
154+
155+
// Step 1: Create or update the activation file
156+
if (!(await fs.pathExists(startupFile))) {
157+
// Create new file with our content
158+
await fs.writeFile(startupFile, activationContent);
159+
traceInfo(`Created new CMD activation file at: ${startupFile}\r\n${activationContent}`);
160+
} else {
161+
// Update existing file if it doesn't have our content
162+
const content = await fs.readFile(startupFile, 'utf8');
163+
if (!content.includes(key)) {
164+
await fs.writeFile(startupFile, `${content}${activationContent}`);
165+
traceInfo(`Updated existing CMD activation file at: ${startupFile}\r\n${activationContent}`);
166+
} else {
167+
traceInfo(`CMD activation file at ${startupFile} already contains activation code`);
168+
}
169+
}
170+
171+
// Step 2: Get existing AutoRun content
172+
const existingAutoRun = await getExistingAutoRun();
173+
174+
// Step 3: Create or update the main batch file
175+
const mainBatchContent = getMainBatchFileContent(startupFile, existingAutoRun);
176+
await fs.writeFile(mainBatchFile, mainBatchContent);
177+
traceInfo(`Created/Updated main batch file at: ${mainBatchFile}`);
178+
179+
// Step 4: Setup registry AutoRun to call our main batch file
180+
const registrySetup = await setupRegistryAutoRun(mainBatchFile);
181+
182+
return registrySetup;
183+
} catch (err) {
184+
traceVerbose(`Failed to setup CMD startup`, err);
185+
return false;
186+
}
187+
}
188+
189+
async function removeCmdStartup(startupFile: string): Promise<boolean> {
190+
let success = true;
191+
192+
// Remove from activation file if it exists
193+
if (await fs.pathExists(startupFile)) {
194+
try {
195+
const content = await fs.readFile(startupFile, 'utf8');
196+
if (content.includes(regionStart)) {
197+
// Remove the entire region including newlines
198+
const pattern = new RegExp(`${regionStart}[\\s\\S]*?${regionEnd}\\r?\\n?`, 'g');
199+
const newContent = content.replace(pattern, '');
200+
201+
if (newContent.trim() === '') {
202+
// Delete the file if it's empty after removal
203+
await fs.remove(startupFile);
204+
traceInfo(`Removed CMD activation file: ${startupFile}`);
205+
} else {
206+
await fs.writeFile(startupFile, newContent);
207+
traceInfo(`Removed activation from CMD activation file at: ${startupFile}`);
208+
}
209+
}
210+
} catch (err) {
211+
traceVerbose(`Failed to remove CMD activation file content`, err);
212+
success = false;
213+
}
214+
}
215+
216+
// Note: We deliberately DO NOT remove the main batch file or registry AutoRun setting
217+
// This allows other components to continue using the AutoRun functionality
218+
219+
return success;
220+
}
221+
222+
export class CmdStartupProvider implements ShellStartupProvider {
223+
public readonly name: string = 'Command Prompt';
224+
private readonly cmdActivationEnvVarKey = 'VSCODE_CMD_ACTIVATE';
225+
226+
async isSetup(): Promise<ShellSetupState> {
227+
const isInstalled = await isCmdInstalled();
228+
if (!isInstalled) {
229+
traceVerbose('CMD is not installed or not on Windows');
230+
return ShellSetupState.NotInstalled;
231+
}
232+
233+
try {
234+
const { startupFile, mainBatchFile } = await getCmdFilePaths();
235+
const isSetup = await isCmdStartupSetup(startupFile, mainBatchFile, this.cmdActivationEnvVarKey);
236+
return isSetup ? ShellSetupState.Setup : ShellSetupState.NotSetup;
237+
} catch (err) {
238+
traceError('Failed to check if CMD startup is setup', err);
239+
return ShellSetupState.NotSetup;
240+
}
241+
}
242+
243+
async setupScripts(): Promise<ShellScriptEditState> {
244+
const isInstalled = await isCmdInstalled();
245+
if (!isInstalled) {
246+
traceVerbose('CMD is not installed or not on Windows');
247+
return ShellScriptEditState.NotInstalled;
248+
}
249+
250+
try {
251+
const { startupFile, mainBatchFile } = await getCmdFilePaths();
252+
const success = await setupCmdStartup(startupFile, mainBatchFile, this.cmdActivationEnvVarKey);
253+
return success ? ShellScriptEditState.Edited : ShellScriptEditState.NotEdited;
254+
} catch (err) {
255+
traceError('Failed to setup CMD startup', err);
256+
return ShellScriptEditState.NotEdited;
257+
}
258+
}
259+
260+
async teardownScripts(): Promise<ShellScriptEditState> {
261+
const isInstalled = await isCmdInstalled();
262+
if (!isInstalled) {
263+
traceVerbose('CMD is not installed or not on Windows');
264+
return ShellScriptEditState.NotInstalled;
265+
}
266+
267+
try {
268+
const { startupFile } = await getCmdFilePaths();
269+
const success = await removeCmdStartup(startupFile);
270+
return success ? ShellScriptEditState.Edited : ShellScriptEditState.NotEdited;
271+
} catch (err) {
272+
traceError('Failed to remove CMD startup', err);
273+
return ShellScriptEditState.NotEdited;
274+
}
275+
}
276+
277+
async updateEnvVariables(collection: EnvironmentVariableCollection, env: PythonEnvironment): Promise<void> {
278+
try {
279+
const cmdActivation = getActivationCommandForShell(env, TerminalShellType.commandPrompt);
280+
if (cmdActivation) {
281+
const command = getCommandAsString(cmdActivation, '&');
282+
collection.replace(this.cmdActivationEnvVarKey, command);
283+
} else {
284+
collection.delete(this.cmdActivationEnvVarKey);
285+
}
286+
} catch (err) {
287+
traceError('Failed to update CMD environment variables', err);
288+
collection.delete(this.cmdActivationEnvVarKey);
289+
}
290+
}
291+
292+
async removeEnvVariables(envCollection: EnvironmentVariableCollection): Promise<void> {
293+
envCollection.delete(this.cmdActivationEnvVarKey);
294+
}
295+
296+
async getEnvVariables(env?: PythonEnvironment): Promise<Map<string, string | undefined> | undefined> {
297+
if (!env) {
298+
return new Map([[this.cmdActivationEnvVarKey, undefined]]);
299+
}
300+
301+
try {
302+
const cmdActivation = getActivationCommandForShell(env, TerminalShellType.commandPrompt);
303+
return cmdActivation
304+
? new Map([[this.cmdActivationEnvVarKey, getCommandAsString(cmdActivation, '&')]])
305+
: undefined;
306+
} catch (err) {
307+
traceError('Failed to get CMD environment variables', err);
308+
return undefined;
309+
}
310+
}
311+
}

0 commit comments

Comments
 (0)