Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 17 additions & 11 deletions src/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,7 @@ import { getLogsFilePath, writeLogToFile, type LogLevel } from 'src/logging';
import { getMainWindow } from 'src/main-window';
import { popupMenu, setupMenu } from 'src/menu';
import { editSiteViaCli, EditSiteOptions } from 'src/modules/cli/lib/cli-site-editor';
import { isStudioCliInstalled } from 'src/modules/cli/lib/ipc-handlers';
import { STABLE_BIN_DIR_PATH } from 'src/modules/cli/lib/windows-installation-manager';
import { WindowsCliInstallationManager } from 'src/modules/cli/lib/windows-installation-manager';
import { shouldExcludeFromSync, shouldLimitDepth } from 'src/modules/sync/lib/tree-utils';
import { supportedEditorConfig, SupportedEditor } from 'src/modules/user-settings/lib/editor';
import { getUserTerminal } from 'src/modules/user-settings/lib/ipc-handlers';
Expand Down Expand Up @@ -833,17 +832,24 @@ export async function openTerminalAtPath( _event: IpcMainInvokeEvent, targetPath
// Ensure the Studio CLI bin directory is in the PATH for the spawned terminal.
// Child processes inherit the environment from the Electron process, which may have
// been started before the CLI was installed or PATH was updated in the registry.
const isCliInstalled = await isStudioCliInstalled();
let env: NodeJS.ProcessEnv | undefined;
if ( isCliInstalled ) {
const currentPath = process.env.PATH || '';
const pathEntries = currentPath.split( ';' ).map( ( p ) => p.toLowerCase() );
if ( ! pathEntries.includes( STABLE_BIN_DIR_PATH.toLowerCase() ) ) {
env = { ...process.env };
delete env.PATH;
delete env.Path;
env.PATH = `${ STABLE_BIN_DIR_PATH };${ currentPath }`;
try {
const installationManager = new WindowsCliInstallationManager();
const isCliInstalled = await installationManager.isCliInstalled();

if ( isCliInstalled ) {
const currentPath = process.env.PATH || '';
const pathEntries = currentPath.split( ';' ).map( ( p ) => p.toLowerCase() );
const STABLE_BIN_DIR_PATH = installationManager.getStableBinDirPath();
if ( ! pathEntries.includes( STABLE_BIN_DIR_PATH.toLowerCase() ) ) {
env = { ...process.env };
delete env.PATH;
delete env.Path;
env.PATH = `${ STABLE_BIN_DIR_PATH };${ currentPath }`;
}
}
} catch {
// Handle error gracefully
}

return promiseExec( `start "Command Prompt" ${ defaultShell }`, {
Expand Down
69 changes: 42 additions & 27 deletions src/modules/cli/lib/windows-installation-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@ import Registry from 'winreg'; // don't update winreg to 1.2.5 - https://github.
import { getMainWindow } from 'src/main-window';
import { StudioCliInstallationManager } from 'src/modules/cli/lib/ipc-handlers';

// `STABLE_BIN_DIR_PATH` resolves to C:\Users\<USERNAME>\AppData\Local\studio\bin
export const STABLE_BIN_DIR_PATH = path.resolve( path.dirname( app.getPath( 'exe' ) ), '../bin' );
const PATH_KEY = 'Path';

const REGISTRY_PATH_KEY = 'Path';
const currentUserRegistry = new Registry( {
hive: Registry.HKCU,
key: '\\Environment',
Expand All @@ -24,13 +21,21 @@ export class WindowsCliInstallationManager implements StudioCliInstallationManag
}
}

getStableBinDirPath() {
if ( ! process.env.LOCALAPPDATA ) {
throw new Error( 'LOCALAPPDATA environment variable is not set' );
}
Comment on lines +25 to +27
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An obvious question is, how do we know that process.env.LOCALAPPDATA is defined? Well, there's no official guarantee, and I can't find any Microsoft documentation on this, but it appears the system sets this variable for each process. It's not editable by users either, AFAICT. That's probably as good a guarantee as we'll get.

Still, it makes sense to ensure that we handle errors thrown from here gracefully, so I'll take a look at that now…

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in b942405


return path.join( process.env.LOCALAPPDATA, 'studio', 'bin' );
}

/**
* Check if the stable bin directory has been created and if it's contained in the registry PATH.
*/
async isCliInstalled(): Promise< boolean > {
try {
const isStudioCliDirInPath = await this.isStudioCliDirInPath();
return isStudioCliDirInPath && existsSync( STABLE_BIN_DIR_PATH );
return isStudioCliDirInPath && existsSync( this.getStableBinDirPath() );
} catch ( error ) {
console.error( 'Failed to check installation status of CLI', error );
return false;
Expand Down Expand Up @@ -105,7 +110,7 @@ export class WindowsCliInstallationManager implements StudioCliInstallationManag

private getPathFromRegistry(): Promise< string > {
return new Promise( ( resolve, reject ) => {
currentUserRegistry.get( PATH_KEY, ( error, item ) => {
currentUserRegistry.get( REGISTRY_PATH_KEY, ( error, item ) => {
if ( error ) {
return reject( error );
}
Expand All @@ -117,18 +122,23 @@ export class WindowsCliInstallationManager implements StudioCliInstallationManag

private setPathInRegistry( updatedPath: string ): Promise< void > {
return new Promise( ( resolve, reject ) => {
currentUserRegistry.set( PATH_KEY, Registry.REG_EXPAND_SZ, updatedPath, ( error ) => {
if ( error ) {
return reject( error );
currentUserRegistry.set(
REGISTRY_PATH_KEY,
Registry.REG_EXPAND_SZ,
updatedPath,
( error ) => {
if ( error ) {
return reject( error );
}

resolve();
}

resolve();
} );
);
} );
}

private async isStudioCliDirInPath(): Promise< boolean > {
let studioCliDir = STABLE_BIN_DIR_PATH;
let studioCliDir = this.getStableBinDirPath();

// Return true if we are running the development version of the app and the production CLI is installed
if ( process.env.NODE_ENV !== 'production' && process.env.LOCALAPPDATA ) {
Expand All @@ -153,13 +163,13 @@ export class WindowsCliInstallationManager implements StudioCliInstallationManag
.split( ';' )
.map( ( p ) => p.trim() )
.filter( Boolean )
.concat( STABLE_BIN_DIR_PATH )
.concat( this.getStableBinDirPath() )
.join( ';' );

await this.setPathInRegistry( updatedPath );
} catch ( error ) {
Sentry.captureException( error );
console.error( 'Failed to install CLI path', error );
Sentry.captureException( error );
}
}

Expand All @@ -172,20 +182,18 @@ export class WindowsCliInstallationManager implements StudioCliInstallationManag
*/
private async installProxyBatFile(): Promise< void > {
try {
await mkdir( STABLE_BIN_DIR_PATH, { recursive: true } );
await mkdir( this.getStableBinDirPath(), { recursive: true } );

const versionedCliPath = path.join(
path.dirname( app.getPath( 'exe' ) ),
'resources/bin/studio-cli.bat'
);
const relativeVersionedCliPath = path.relative( STABLE_BIN_DIR_PATH, versionedCliPath );

const content = `@echo off\n"%~dp0\\${ relativeVersionedCliPath }" %*`;
const content = `@echo off\n"${ versionedCliPath }" %*`;

await writeFile( path.join( STABLE_BIN_DIR_PATH, 'studio.bat' ), content );
await writeFile( path.join( this.getStableBinDirPath(), 'studio.bat' ), content );
} catch ( error ) {
console.error( 'Failed to install CLI proxy .bat file', error );
Sentry.captureException( error );
console.error( 'Failed to install CLI: Proxy Bat file', error );
}
}

Expand All @@ -195,13 +203,20 @@ export class WindowsCliInstallationManager implements StudioCliInstallationManag
}

private async uninstallCli(): Promise< void > {
const currentPath = await this.getPathFromRegistry();
const newPath = currentPath
.split( ';' )
.filter( ( item ) => item.trim().toLowerCase() !== STABLE_BIN_DIR_PATH.toLowerCase() )
.join( ';' );
try {
const currentPath = await this.getPathFromRegistry();
const newPath = currentPath
.split( ';' )
.filter(
( item ) => item.trim().toLowerCase() !== this.getStableBinDirPath().toLowerCase()
)
.join( ';' );

await this.setPathInRegistry( newPath );
await this.setPathInRegistry( newPath );
} catch ( error ) {
console.error( 'Failed to uninstall CLI proxy .bat file', error );
Sentry.captureException( error );
}
}
}

Expand Down