From 80106406b7779a213a789a882ac9561fcb27fcca Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Mon, 5 Feb 2024 18:25:34 -0800 Subject: [PATCH 1/8] notification for bundled env updates, auto update bundled env --- src/assets/server-icon.svg | 1 + src/main/app.ts | 52 ++++++- src/main/config/appdata.ts | 10 ++ src/main/config/settings.ts | 4 + src/main/dialog/dialogtitlebar.ts | 22 +-- src/main/dialog/themedwindow.ts | 9 +- src/main/eventtypes.ts | 8 +- src/main/main.ts | 135 +++++++++++++++++- src/main/pythonenvdialog/preload.ts | 2 +- src/main/pythonenvdialog/pythonenvdialog.ts | 33 +---- src/main/pythonenvselectpopup/preload.ts | 16 +++ .../pythonenvselectpopup.ts | 30 ++++ src/main/registry.ts | 35 ++++- src/main/sessionwindow/sessionwindow.ts | 34 +++++ src/main/settingsdialog/preload.ts | 3 + src/main/settingsdialog/settingsdialog.ts | 32 ++++- src/main/titlebarview/preload.ts | 13 ++ src/main/titlebarview/titlebar.html | 18 +++ src/main/titlebarview/titlebarview.ts | 7 + src/main/utils.ts | 9 ++ 20 files changed, 426 insertions(+), 47 deletions(-) create mode 100644 src/assets/server-icon.svg diff --git a/src/assets/server-icon.svg b/src/assets/server-icon.svg new file mode 100644 index 00000000..a78d29d8 --- /dev/null +++ b/src/assets/server-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/app.ts b/src/main/app.ts index 20927e07..91e2cb00 100644 --- a/src/main/app.ts +++ b/src/main/app.ts @@ -16,6 +16,7 @@ import * as yaml from 'js-yaml'; import * as semver from 'semver'; import * as fs from 'fs'; import { + bundledEnvironmentIsInstalled, clearSession, EnvironmentInstallStatus, getBundledPythonEnvPath, @@ -392,6 +393,12 @@ export class JupyterApplication implements IApplication, IDisposable { installUpdatesAutomatically: settings.getValue( SettingType.installUpdatesAutomatically ), + notifyOnBundledEnvUpdates: settings.getValue( + SettingType.notifyOnBundledEnvUpdates + ), + updateBundledEnvAutomatically: settings.getValue( + SettingType.updateBundledEnvAutomatically + ), defaultWorkingDirectory: userSettings.getValue( SettingType.defaultWorkingDirectory ), @@ -429,7 +436,9 @@ export class JupyterApplication implements IApplication, IDisposable { isDarkTheme: this._isDarkTheme, defaultPythonPath: userSettings.getValue(SettingType.pythonPath), app: this, - activateTab + activateTab, + bundledEnvInstallationExists: bundledEnvironmentIsInstalled(), + bundledEnvInstallationLatest: this._registry.bundledEnvironmentIsLatest() }); this._managePythonEnvDialog = dialog; @@ -529,7 +538,16 @@ export class JupyterApplication implements IApplication, IDisposable { }; dialog.showMessageBox(dialogOpts).then(returnValue => { - if (returnValue.response === 0) autoUpdater.quitAndInstall(); + if (returnValue.response === 0) { + if ( + userSettings.getValue(SettingType.updateBundledEnvAutomatically) && + bundledEnvironmentIsInstalled() + ) { + appData.updateBundledEnvOnRestart = true; + appData.save(); + } + autoUpdater.quitAndInstall(); + } }); }); @@ -773,6 +791,27 @@ export class JupyterApplication implements IApplication, IDisposable { } ); + this._evm.registerEventHandler( + EventTypeMain.UpdateBundledPythonEnv, + async event => { + const choice = dialog.showMessageBoxSync({ + type: 'warning', + message: 'Update bundled environment', + detail: + 'App will restart and the existing environment installation will be deleted before update. Would you like to continue?', + buttons: ['Update', 'Cancel'], + defaultId: 1, + cancelId: 1 + }); + + if (choice === 0) { + appData.updateBundledEnvOnRestart = true; + app.relaunch(); + app.quit(); + } + } + ); + this._evm.registerEventHandler( EventTypeMain.ShowManagePythonEnvironmentsDialog, async (event, activateTab) => { @@ -1091,6 +1130,15 @@ export class JupyterApplication implements IApplication, IDisposable { } ); + this._evm.registerEventHandler( + EventTypeMain.SetSettings, + (_event, settings: { [key: string]: any }) => { + for (const key in settings) { + userSettings.setValue(key as SettingType, settings[key]); + } + } + ); + this._evm.registerSyncEventHandler( EventTypeMain.GetServerInfo, (event): IServerInfo => { diff --git a/src/main/config/appdata.ts b/src/main/config/appdata.ts index cb66b83d..7995225e 100644 --- a/src/main/config/appdata.ts +++ b/src/main/config/appdata.ts @@ -169,6 +169,10 @@ export class ApplicationData { }); } } + + if ('updateBundledEnvOnRestart' in jsonData) { + this.updateBundledEnvOnRestart = jsonData.updateBundledEnvOnRestart; + } } save() { @@ -245,6 +249,10 @@ export class ApplicationData { }); } + if (this.updateBundledEnvOnRestart) { + appDataJSON.updateBundledEnvOnRestart = true; + } + fs.writeFileSync(appDataPath, JSON.stringify(appDataJSON, null, 2)); } @@ -396,6 +404,8 @@ export class ApplicationData { discoveredPythonEnvs: IPythonEnvironment[] = []; userSetPythonEnvs: IPythonEnvironment[] = []; + updateBundledEnvOnRestart: boolean = false; + private _recentSessionsChanged = new Signal(this); } diff --git a/src/main/config/settings.ts b/src/main/config/settings.ts index 8045f2ea..1d5a344b 100644 --- a/src/main/config/settings.ts +++ b/src/main/config/settings.ts @@ -40,6 +40,8 @@ export type KeyValueMap = { [key: string]: string }; export enum SettingType { checkForUpdatesAutomatically = 'checkForUpdatesAutomatically', installUpdatesAutomatically = 'installUpdatesAutomatically', + notifyOnBundledEnvUpdates = 'notifyOnBundledEnvUpdates', + updateBundledEnvAutomatically = 'updateBundledEnvAutomatically', theme = 'theme', syncJupyterLabTheme = 'syncJupyterLabTheme', @@ -124,6 +126,8 @@ export class UserSettings { this._settings = { checkForUpdatesAutomatically: new Setting(true), installUpdatesAutomatically: new Setting(true), + notifyOnBundledEnvUpdates: new Setting(true), + updateBundledEnvAutomatically: new Setting(false), showNewsFeed: new Setting(true), /* making themes workspace overridable is not feasible. diff --git a/src/main/dialog/dialogtitlebar.ts b/src/main/dialog/dialogtitlebar.ts index d9317aed..fd40c920 100644 --- a/src/main/dialog/dialogtitlebar.ts +++ b/src/main/dialog/dialogtitlebar.ts @@ -15,13 +15,17 @@ class JupyterLabDialogTitleBar extends HTMLElement { const titleEl = document.createElement('div'); titleEl.setAttribute('class', 'dialog-title'); - const closeButton = document.createElement('div'); - closeButton.setAttribute('class', 'close-button'); - closeButton.innerHTML = ``; - closeButton.title = 'Close'; - closeButton.onclick = () => { - window.close(); - }; + const closable = this.getAttribute('data-closable') !== 'false'; + let closeButton = null; + if (closable) { + closeButton = document.createElement('div'); + closeButton.setAttribute('class', 'close-button'); + closeButton.innerHTML = ``; + closeButton.title = 'Close'; + closeButton.onclick = () => { + window.close(); + }; + } const title = this.getAttribute('data-title'); titleEl.textContent = title; @@ -85,7 +89,9 @@ class JupyterLabDialogTitleBar extends HTMLElement { shadow.appendChild(style); shadow.appendChild(wrapper); wrapper.appendChild(titleEl); - wrapper.appendChild(closeButton); + if (closeButton) { + wrapper.appendChild(closeButton); + } } } diff --git a/src/main/dialog/themedwindow.ts b/src/main/dialog/themedwindow.ts index f6eb3559..afa65ee9 100644 --- a/src/main/dialog/themedwindow.ts +++ b/src/main/dialog/themedwindow.ts @@ -16,6 +16,7 @@ export class ThemedWindow { width: options.width, height: options.height, show: false, + closable: options.closable !== false, resizable: options.resizable !== false, titleBarStyle: 'hidden', frame: process.platform === 'darwin', @@ -40,6 +41,11 @@ export class ThemedWindow { return this._window; } + close() { + this._window.closable = true; + this._window.close(); + } + loadDialogContent(bodyHtml: string) { let toolkitJsSrc = fs .readFileSync( @@ -114,7 +120,7 @@ export class ThemedWindow {
@@ -141,6 +147,7 @@ export namespace ThemedWindow { title: string; width: number; height: number; + closable?: boolean; resizable?: boolean; preload?: string; } diff --git a/src/main/eventtypes.ts b/src/main/eventtypes.ts index 7c1d251d..7234ae7e 100644 --- a/src/main/eventtypes.ts +++ b/src/main/eventtypes.ts @@ -39,6 +39,7 @@ export enum EventTypeMain { SetDefaultWorkingDirectory = 'set-default-working-directory', SelectPythonPath = 'select-python-path', InstallBundledPythonEnv = 'install-bundled-python-env', + UpdateBundledPythonEnv = 'update-bundled-python-env', ValidatePythonPath = 'validate-python-path', ValidateRemoteServerUrl = 'validate-remote-server-url', SetDefaultPythonPath = 'set-default-python-path', @@ -79,7 +80,8 @@ export enum EventTypeMain { ValidateSystemPythonPath = 'validate-system-python-path', SetSystemPythonPath = 'set-system-python-path', CopySessionInfoToClipboard = 'copy-session-info-to-clipboard', - RestartSession = 'restart-session' + RestartSession = 'restart-session', + SetSettings = 'set-settings' } // events sent to Renderer process @@ -94,6 +96,7 @@ export enum EventTypeRenderer { SetTitle = 'set-title', SetActive = 'set-active', ShowServerStatus = 'show-server-status', + ShowServerNotificationBadge = 'show-server-notification-badge', SetRecentSessionList = 'set-recent-session-list', SetNewsList = 'set-news-list', SetNotificationMessage = 'set-notification-message', @@ -101,5 +104,6 @@ export enum EventTypeRenderer { SetDefaultWorkingDirectoryResult = 'set-default-working-directory-result', ResetPythonEnvSelectPopup = 'reset-python-env-select-popup', SetPythonEnvironmentList = 'set-python-environment-list', - SetEnvironmentListUpdateStatus = 'set-environment-list-update-status' + SetEnvironmentListUpdateStatus = 'set-environment-list-update-status', + ShowUpdateBundledEnvAction = 'show-bundled-env-action' } diff --git a/src/main/main.ts b/src/main/main.ts index da938174..00faff06 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -1,14 +1,28 @@ import { app, Menu, MenuItem } from 'electron'; import log, { LevelOption } from 'electron-log'; import * as fs from 'fs'; -import { getAppDir, isDevMode, waitForFunction } from './utils'; +import * as semver from 'semver'; +import { + bundledEnvironmentIsInstalled, + EnvironmentInstallStatus, + getAppDir, + getBundledPythonEnvPath, + getBundledPythonPath, + installBundledEnvironment, + isDevMode, + versionWithoutSuffix, + waitForDuration, + waitForFunction +} from './utils'; import { execSync } from 'child_process'; import { JupyterApplication } from './app'; import { ICLIArguments } from './tokens'; import { SessionConfig } from './config/sessionconfig'; import { SettingType, userSettings } from './config/settings'; import { parseCLIArgs } from './cli'; -import { getPythonEnvsDirectory } from './env'; +import { getPythonEnvsDirectory, runCommandInEnvironment } from './env'; +import { ThemedWindow } from './dialog/themedwindow'; +import { appData } from './config/appdata'; let jupyterApp: JupyterApplication; let fileToOpenInMainInstance = ''; @@ -192,6 +206,7 @@ app.on('ready', async () => { try { await handleMultipleAppInstances(); + await updateBundledPythonEnvInstallation(); redirectConsoleToLog(); setApplicationMenu(); setupJLabCommand(); @@ -259,3 +274,119 @@ function handleMultipleAppInstances(): Promise { } }); } + +async function needToUpdateBundledPythonEnvInstallation(): Promise { + // update on restart requested + if (appData.updateBundledEnvOnRestart) { + return true; + } + + // update if auto update is + if ( + !( + userSettings.getValue(SettingType.updateBundledEnvAutomatically) && + bundledEnvironmentIsInstalled() + ) + ) { + return false; + } + + const appDataEnvironments = [ + ...appData.discoveredPythonEnvs, + ...appData.userSetPythonEnvs + ]; + const bundledPythonPath = getBundledPythonPath(); + const bundledEnvInAppData = appDataEnvironments.find( + env => bundledPythonPath === env.path + ); + + const appVersion = app.getVersion(); + + // if the version in appData is latest, then assume it is latest + if (bundledEnvInAppData) { + const jlabVersionInAppData = bundledEnvInAppData.versions['jupyterlab']; + + if ( + semver.compare( + versionWithoutSuffix(jlabVersionInAppData), + versionWithoutSuffix(appVersion) + ) === -1 + ) { + return true; + } + } + + // if not latest in appData check the active jupyterlab version + // in case appData is outdated + let outputVersion = ''; + if ( + await runCommandInEnvironment( + getBundledPythonEnvPath(), + "python -c 'import jupyterlab; print(jupyterlab.__version__)'", + { + stdout: msg => { + outputVersion += msg; + } + } + ) + ) { + if ( + semver.compare( + versionWithoutSuffix(outputVersion.trim()), + versionWithoutSuffix(appVersion) + ) === -1 + ) { + return true; + } + } + + return false; +} + +async function updateBundledPythonEnvInstallation() { + if (!(await needToUpdateBundledPythonEnvInstallation())) { + return; + } + + const statusDialog = new ThemedWindow({ + isDarkTheme: true, + title: 'Updating bundled environment installation', + width: 400, + height: 150, + closable: false + }); + + const setStatusMessage = (message: string) => { + statusDialog.loadDialogContent(message); + waitForDuration(100); + }; + + setStatusMessage('Reinstalling environment.'); + + const installPath = getBundledPythonEnvPath(); + await installBundledEnvironment(installPath, { + onInstallStatus: (status, message) => { + log.info(`Bundled env install status: ${status}, message ${message}`); + switch (status) { + case EnvironmentInstallStatus.RemovingExistingInstallation: + setStatusMessage('Removing existing installation...'); + break; + case EnvironmentInstallStatus.Started: + setStatusMessage('Installing new version...'); + break; + case EnvironmentInstallStatus.Success: + { + appData.updateBundledEnvOnRestart = false; + setStatusMessage('Finished updating.'); + setTimeout(() => { + statusDialog.close(); + }, 2000); + } + break; + } + }, + get forceOverwrite() { + return true; + } + }); +} diff --git a/src/main/pythonenvdialog/preload.ts b/src/main/pythonenvdialog/preload.ts index 0546ea5f..5a56c553 100644 --- a/src/main/pythonenvdialog/preload.ts +++ b/src/main/pythonenvdialog/preload.ts @@ -73,7 +73,7 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.send(EventTypeMain.InstallBundledPythonEnv, envPath); }, updateBundledPythonEnv: () => { - ipcRenderer.send(EventTypeMain.InstallBundledPythonEnv); + ipcRenderer.send(EventTypeMain.UpdateBundledPythonEnv); }, onInstallBundledPythonEnvStatus: ( callback: InstallBundledPythonEnvStatusListener diff --git a/src/main/pythonenvdialog/pythonenvdialog.ts b/src/main/pythonenvdialog/pythonenvdialog.ts index 10d18b04..eae41f02 100644 --- a/src/main/pythonenvdialog/pythonenvdialog.ts +++ b/src/main/pythonenvdialog/pythonenvdialog.ts @@ -3,7 +3,6 @@ import * as ejs from 'ejs'; import { - app, BrowserWindow, clipboard, dialog, @@ -22,7 +21,6 @@ import { isEnvInstalledByDesktopApp, launchTerminalInDirectory, openDirectoryInExplorer, - versionWithoutSuffix, waitForDuration } from '../utils'; import { EventManager } from '../eventmanager'; @@ -54,36 +52,13 @@ export class ManagePythonEnvironmentDialog { if (defaultPythonPath === '') { defaultPythonPath = bundledPythonPath; } - let bundledEnvInstallationExists = false; - try { - bundledEnvInstallationExists = fs.existsSync(bundledPythonPath); - } catch (error) { - console.error('Failed to check for bundled Python path', error); - } + const bundledEnvInstallationExists = options.bundledEnvInstallationExists; const selectBundledPythonPath = (defaultPythonPath === '' || defaultPythonPath === bundledPythonPath) && bundledEnvInstallationExists; - let bundledEnvInstallationLatest = true; - - if (bundledEnvInstallationExists) { - try { - const bundledEnv = this._app.registry.getEnvironmentByPath( - bundledPythonPath - ); - const jlabVersion = bundledEnv.versions['jupyterlab']; - const appVersion = app.getVersion(); - - if ( - versionWithoutSuffix(jlabVersion) !== versionWithoutSuffix(appVersion) - ) { - bundledEnvInstallationLatest = false; - } - } catch (error) { - console.error('Failed to check bundled environment update', error); - } - } + const bundledEnvInstallationLatest = options.bundledEnvInstallationLatest; const infoIconSrc = fs.readFileSync( path.join(__dirname, '../../../app-assets/info-icon.svg') @@ -804,8 +779,6 @@ export class ManagePythonEnvironmentDialog { } function handleUpdateBundledEv() { - installingJupyterLabServerEnv = true; - showBundledEnvInstallProgress('Updating environment', true); window.electronAPI.updateBundledPythonEnv(); } @@ -1399,5 +1372,7 @@ export namespace ManagePythonEnvironmentDialog { envs: IPythonEnvironment[]; defaultPythonPath: string; activateTab?: Tab; + bundledEnvInstallationExists: boolean; + bundledEnvInstallationLatest: boolean; } } diff --git a/src/main/pythonenvselectpopup/preload.ts b/src/main/pythonenvselectpopup/preload.ts index 3e1d09b1..fecc6106 100644 --- a/src/main/pythonenvselectpopup/preload.ts +++ b/src/main/pythonenvselectpopup/preload.ts @@ -10,11 +10,13 @@ type CurrentPythonPathSetListener = ( type ResetPythonEnvSelectPopupListener = () => void; type CustomPythonPathSelectedListener = (path: string) => void; type SetPythonEnvironmentListListener = (envs: IPythonEnvironment[]) => void; +type ShowUpdateBundledEnvActionListener = (show: boolean) => void; let onCustomPythonPathSelectedListener: CustomPythonPathSelectedListener; let onCurrentPythonPathSetListener: CurrentPythonPathSetListener; let onResetPythonEnvSelectPopupListener: ResetPythonEnvSelectPopupListener; let onSetPythonEnvironmentListListener: SetPythonEnvironmentListListener; +let onShowUpdateBundledEnvActionListener: ShowUpdateBundledEnvActionListener; contextBridge.exposeInMainWorld('electronAPI', { getAppConfig: () => { @@ -56,6 +58,14 @@ contextBridge.exposeInMainWorld('electronAPI', { }, copySessionInfo: () => { ipcRenderer.send(EventTypeMain.CopySessionInfoToClipboard); + }, + updateBundledPythonEnv: () => { + ipcRenderer.send(EventTypeMain.UpdateBundledPythonEnv); + }, + onShowUpdateBundledEnvAction: ( + callback: ShowUpdateBundledEnvActionListener + ) => { + onShowUpdateBundledEnvActionListener = callback; } }); @@ -86,4 +96,10 @@ ipcRenderer.on(EventTypeRenderer.SetPythonEnvironmentList, (event, envs) => { } }); +ipcRenderer.on(EventTypeRenderer.ShowUpdateBundledEnvAction, (event, show) => { + if (onShowUpdateBundledEnvActionListener) { + onShowUpdateBundledEnvActionListener(show); + } +}); + export {}; diff --git a/src/main/pythonenvselectpopup/pythonenvselectpopup.ts b/src/main/pythonenvselectpopup/pythonenvselectpopup.ts index 04c10d42..c4b99d98 100644 --- a/src/main/pythonenvselectpopup/pythonenvselectpopup.ts +++ b/src/main/pythonenvselectpopup/pythonenvselectpopup.ts @@ -37,6 +37,10 @@ export class PythonEnvironmentSelectPopup { path.join(__dirname, '../../../app-assets/xmark.svg') ); + const serverIconSrc = fs.readFileSync( + path.join(__dirname, '../../../app-assets/server-icon.svg') + ); + const template = `
@@ -346,6 +360,12 @@ export class SettingsDialog {
<%= installUpdatesAutomaticallyEnabled ? '' : 'disabled' %>>Download and install updates automatically
+
+ onchange='handleAutoCheckForUpdates(this);'>Show bundled environment update notifications
${infoIconSrc}
+
+
+ >Update bundled environment automatically when app is updated
${infoIconSrc}
+
Check for updates now
@@ -354,6 +374,8 @@ export class SettingsDialog {