Skip to content
Closed
28 changes: 17 additions & 11 deletions apps/studio/src/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,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 @@ -982,17 +981,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
75 changes: 46 additions & 29 deletions apps/studio/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,22 @@ 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
Copy Markdown
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
Copy Markdown
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


// Resolves to C:\Users\<USERNAME>\AppData\Local\studio\bin
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 +111,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 +123,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 +164,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 +183,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"${ versionedCliPath }" %*`;

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

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,15 +204,23 @@ 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 );
if ( process.env.NODE_ENV === 'production' ) {
await rm( STABLE_BIN_DIR_PATH, { recursive: true, force: true } );
if ( process.env.NODE_ENV === 'production' ) {
await rm( this.getStableBinDirPath(), { recursive: true, force: true } );
}
} catch ( error ) {
console.error( 'Failed to uninstall CLI proxy .bat file', error );
Sentry.captureException( error );
}
}
}
Expand Down