Skip to content

Commit

Permalink
setup CLI from UI on macOS
Browse files Browse the repository at this point in the history
  • Loading branch information
mbektas committed Feb 21, 2024
1 parent 0a2fabd commit c8ca289
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 22 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"dist:osx-64": "yarn dist:osx",
"dist:osx-arm64": "yarn dist:osx",
"dist:osx-dev": "yarn build && CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --macos --publish never",
"dist:osx-arm64-dev": "yarn build && CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --macos --arm64 --publish never",
"dist:win-64": "yarn build && electron-builder --win --publish never",
"dist:win-arm64": "yarn build && yarn electron-builder --arm64 --publish never",
"update_workflow_conda_lock": "cd workflow_env && rimraf *.lock && conda-lock --kind explicit -f publish_env.yaml && cd -",
Expand Down
24 changes: 24 additions & 0 deletions src/main/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
installBundledEnvironment,
isDarkTheme,
pythonPathForEnvPath,
setupJlabCLICommandWithElevatedRights,
waitForDuration
} from './utils';
import { IServerFactory, JupyterServerFactory } from './server';
Expand Down Expand Up @@ -1201,6 +1202,29 @@ export class JupyterApplication implements IApplication, IDisposable {
return true;
}
);

this._evm.registerSyncEventHandler(
EventTypeMain.SetupCLICommandWithElevatedRights,
async event => {
const showSetupErrorMessage = () => {
dialog.showErrorBox(
'CLI setup error',
'Failed to setup jlab CLI command! Please see logs for details.'
);
};

try {
const succeeded = await setupJlabCLICommandWithElevatedRights();
if (!succeeded) {
showSetupErrorMessage();
}
return succeeded;
} catch (error) {
showSetupErrorMessage();
return false;
}
}
);
}

private _showUpdateDialog(
Expand Down
3 changes: 2 additions & 1 deletion src/main/eventtypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ export enum EventTypeMain {
SetSystemPythonPath = 'set-system-python-path',
CopySessionInfoToClipboard = 'copy-session-info-to-clipboard',
RestartSession = 'restart-session',
SetSettings = 'set-settings'
SetSettings = 'set-settings',
SetupCLICommandWithElevatedRights = 'setup-cli-command'
}

// events sent to Renderer process
Expand Down
22 changes: 4 additions & 18 deletions src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ import * as semver from 'semver';
import {
bundledEnvironmentIsInstalled,
EnvironmentInstallStatus,
getAppDir,
getBundledPythonEnvPath,
getBundledPythonPath,
installBundledEnvironment,
isDevMode,
jlabCLICommandIsSetup,
setupJlabCommandWithUserRights,
versionWithoutSuffix,
waitForDuration,
waitForFunction
} from './utils';
import { execSync } from 'child_process';
import { JupyterApplication } from './app';
import { ICLIArguments } from './tokens';
import { SessionConfig } from './config/sessionconfig';
Expand Down Expand Up @@ -130,25 +130,11 @@ function setupJLabCommand() {
return;
}

const symlinkPath = '/usr/local/bin/jlab';
const targetPath = `${getAppDir()}/app/jlab`;

if (!fs.existsSync(targetPath)) {
if (jlabCLICommandIsSetup()) {
return;
}

try {
if (!fs.existsSync(symlinkPath)) {
const cmd = `ln -s ${targetPath} ${symlinkPath}`;
execSync(cmd, { shell: '/bin/bash' });
fs.chmodSync(symlinkPath, 0o755);
}

// after a DMG install, mode resets
fs.chmodSync(targetPath, 0o755);
} catch (error) {
log.error(error);
}
setupJlabCommandWithUserRights();
}

function createPythonEnvsDirectory() {
Expand Down
3 changes: 3 additions & 0 deletions src/main/settingsdialog/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
},
setSettings: (settings: { [key: string]: any }) => {
ipcRenderer.send(EventTypeMain.SetSettings, settings);
},
setupCLICommand: () => {
return ipcRenderer.invoke(EventTypeMain.SetupCLICommandWithElevatedRights);
}
});

Expand Down
41 changes: 39 additions & 2 deletions src/main/settingsdialog/settingsdialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ import {
ThemeType
} from '../config/settings';
import { IRegistry } from '../registry';
import { jlabCLICommandIsSetup } from '../utils';

export class SettingsDialog {
constructor(options: SettingsDialog.IOptions, registry: IRegistry) {
this._window = new ThemedWindow({
isDarkTheme: options.isDarkTheme,
title: 'Settings',
width: 700,
height: 450,
height: 500,
preload: path.join(__dirname, './preload.js')
});

Expand All @@ -45,6 +46,7 @@ export class SettingsDialog {
const installUpdatesAutomaticallyEnabled = process.platform === 'darwin';
const installUpdatesAutomatically =
installUpdatesAutomaticallyEnabled && options.installUpdatesAutomatically;
const cliCommandIsSetup = jlabCLICommandIsSetup();

let strServerEnvVars = '';
if (Object.keys(serverEnvVars).length > 0) {
Expand Down Expand Up @@ -336,6 +338,19 @@ export class SettingsDialog {
</jp-radio-group>
</div>
<div class="row setting-section">
<div class="row">
<label>jlab CLI</label>
</div>
<div class="row">
<div id="setup-cli-command-button">
<jp-button onclick='handleSetupCLICommand(this);'>Setup CLI</jp-button>
</div>
<div id="setup-cli-command-label" style="flex-grow: 1"></div>
</div>
</div>
<div class="row setting-section">
<div class="row">
<label for="log-level">Log level</label>
Expand Down Expand Up @@ -376,6 +391,9 @@ export class SettingsDialog {
const autoInstallCheckbox = document.getElementById('checkbox-update-install');
const notifyOnBundledEnvUpdatesCheckbox = document.getElementById('notify-on-bundled-env-updates');
const updateBundledEnvAutomaticallyCheckbox = document.getElementById('update-bundled-env-automatically');
const setupCLICommandButton = document.getElementById('setup-cli-command-button');
const setupCLICommandLabel = document.getElementById('setup-cli-command-label');
let cliCommandIsSetup = <%= cliCommandIsSetup %>;
function handleAutoCheckForUpdates(el) {
updateAutoInstallCheckboxState();
Expand All @@ -398,11 +416,29 @@ export class SettingsDialog {
window.electronAPI.showLogs();
}
function handleSetupCLICommand(el) {
window.electronAPI.setupCLICommand().then(result => {
cliCommandIsSetup = result;
updateCLICommandSetupStatus();
});
}
function updateCLICommandSetupStatus() {
if (cliCommandIsSetup) {
setupCLICommandButton.style.display = 'none';
setupCLICommandLabel.innerHTML = '<b>jlab</b> CLI command is ready to use in your system terminal!';
} else {
setupCLICommandButton.style.display = 'block';
setupCLICommandLabel.innerHTML = 'CLI command is not set up yet. Click to set up now. This requires elevated permissions.';
}
}
function onLogLevelChanged(el) {
window.electronAPI.setLogLevel(el.value);
}
document.addEventListener("DOMContentLoaded", () => {
updateCLICommandSetupStatus();
updateAutoInstallCheckboxState();
});
</script>
Expand Down Expand Up @@ -480,7 +516,8 @@ export class SettingsDialog {
serverArgs,
overrideDefaultServerArgs,
serverEnvVars: strServerEnvVars,
ctrlWBehavior
ctrlWBehavior,
cliCommandIsSetup
});
}

Expand Down
122 changes: 121 additions & 1 deletion src/main/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ import log from 'electron-log';
import { AddressInfo, createServer, Socket } from 'net';
import { app, nativeTheme } from 'electron';
import { IPythonEnvironment } from './tokens';
import { exec, execFile, ExecFileOptions, execFileSync } from 'child_process';
import {
exec,
execFile,
ExecFileOptions,
execFileSync,
execSync
} from 'child_process';

export const DarkThemeBGColor = '#212121';
export const LightThemeBGColor = '#ffffff';
Expand Down Expand Up @@ -710,3 +716,117 @@ export function launchTerminalInDirectory(
exec(`gnome-terminal --working-directory="${dirPath}"${callCommands}`);
}
}

export function getJlabCLICommandSymlinkPath(): string {
if (process.platform === 'darwin') {
return '/usr/local/bin/jlab';
}
}

export function getJlabCLICommandTargetPath(): string {
if (process.platform === 'darwin') {
return `${getAppDir()}/app/jlab`;
}
}

export function jlabCLICommandIsSetup(): boolean {
if (process.platform !== 'darwin') {
return true;
}

const symlinkPath = getJlabCLICommandSymlinkPath();
const targetPath = getJlabCLICommandTargetPath();

if (!fs.existsSync(symlinkPath)) {
return false;
}

const stats = fs.lstatSync(symlinkPath);
if (!stats.isSymbolicLink()) {
return false;
}

try {
fs.accessSync(targetPath, fs.constants.X_OK);
} catch (error) {
log.error('App CLI is not executable', error);
return false;
}

return fs.readlinkSync(symlinkPath) === targetPath;
}

export async function setupJlabCLICommandWithElevatedRights(): Promise<
boolean
> {
if (process.platform !== 'darwin') {
return false;
}

const symlinkPath = getJlabCLICommandSymlinkPath();
const targetPath = getJlabCLICommandTargetPath();

if (!fs.existsSync(targetPath)) {
log.error(`Target path "${targetPath}" does not exist! `);
return false;
}

const shellCommands: string[] = [];
const symlinkParentDir = path.dirname(symlinkPath);

// create parent directory
if (!fs.existsSync(symlinkParentDir)) {
shellCommands.push(`mkdir -p ${symlinkParentDir}`);
}

// create symlink
shellCommands.push(`ln -f -s \\"${targetPath}\\" \\"${symlinkPath}\\"`);

// make files executable
shellCommands.push(`chmod 755 \\"${symlinkPath}\\"`);
shellCommands.push(`chmod 755 \\"${targetPath}\\"`);

const command = `do shell script "${shellCommands.join(
' && '
)}" with administrator privileges`;

return new Promise<boolean>((resolve, reject) => {
const cliSetupProc = exec(`osascript -e '${command}'`);

cliSetupProc.on('exit', (exitCode: number) => {
if (exitCode === 0) {
resolve(true);
} else {
log.error(`Failed to setup CLI with exit code ${exitCode}`);
reject();
}
});

cliSetupProc.on('error', (err: Error) => {
log.error(err);
reject();
});
});
}

export async function setupJlabCommandWithUserRights() {
const symlinkPath = getJlabCLICommandSymlinkPath();
const targetPath = getJlabCLICommandTargetPath();

if (!fs.existsSync(targetPath)) {
return;
}

try {
if (!fs.existsSync(symlinkPath)) {
const cmd = `ln -s ${targetPath} ${symlinkPath}`;
execSync(cmd, { shell: '/bin/bash' });
fs.chmodSync(symlinkPath, 0o755);
}

// after a DMG install, mode resets
fs.chmodSync(targetPath, 0o755);
} catch (error) {
log.error(error);
}
}

0 comments on commit c8ca289

Please sign in to comment.