diff --git a/.eslintrc.js b/.eslintrc.js index 6be699cf7..fbf46eda5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -17,7 +17,6 @@ module.exports = { 'scripts/*', 'electron/*', 'electron-app/*', - 'browser-app/*', 'plugins/*', 'arduino-ide-extension/src/node/cli-protocol', ], diff --git a/.vscode/launch.json b/.vscode/launch.json index 820ba07b8..06f959e85 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -80,37 +80,6 @@ "port": 9222, "webRoot": "${workspaceFolder}/electron-app" }, - { - "type": "node", - "request": "launch", - "name": "App (Browser)", - "program": "${workspaceRoot}/browser-app/src-gen/backend/main.js", - "args": [ - "--hostname=0.0.0.0", - "--port=3000", - "--no-cluster", - "--no-app-auto-install", - "--plugins=local-dir:plugins" - ], - "windows": { - "env": { - "NODE_ENV": "development", - "NODE_PRESERVE_SYMLINKS": "1" - } - }, - "env": { - "NODE_ENV": "development" - }, - "sourceMaps": true, - "outFiles": [ - "${workspaceRoot}/browser-app/src-gen/backend/*.js", - "${workspaceRoot}/browser-app/lib/**/*.js", - "${workspaceRoot}/arduino-ide-extension/lib/**/*.js" - ], - "smartStep": true, - "internalConsoleOptions": "openOnSessionStart", - "outputCapture": "std" - }, { "type": "node", "request": "launch", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index de5ee90fc..9d3e8e0db 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -12,17 +12,6 @@ "clear": false } }, - { - "label": "Arduino IDE - Start Browser App", - "type": "shell", - "command": "yarn --cwd ./browser-app start", - "group": "build", - "presentation": { - "reveal": "always", - "panel": "new", - "clear": true - } - }, { "label": "Arduino IDE - Watch IDE Extension", "type": "shell", @@ -34,17 +23,6 @@ "clear": false } }, - { - "label": "Arduino IDE - Watch Browser App", - "type": "shell", - "command": "yarn --cwd ./browser-app watch", - "group": "build", - "presentation": { - "reveal": "always", - "panel": "new", - "clear": false - } - }, { "label": "Arduino IDE - Watch Electron App", "type": "shell", @@ -56,14 +34,6 @@ "clear": false } }, - { - "label": "Arduino IDE - Watch All [Browser]", - "type": "shell", - "dependsOn": [ - "Arduino IDE - Watch IDE Extension", - "Arduino IDE - Watch Browser App" - ] - }, { "label": "Arduino IDE - Watch All [Electron]", "type": "shell", diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index 8f41acb7b..ac951c619 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -147,11 +147,9 @@ "frontend": "lib/browser/arduino-ide-frontend-module" }, { - "frontend": "lib/browser/theia/core/browser-menu-module", "frontendElectron": "lib/electron-browser/theia/core/electron-menu-module" }, { - "frontend": "lib/browser/theia/core/browser-window-module", "frontendElectron": "lib/electron-browser/theia/core/electron-window-module" }, { diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index 9ff8a5389..000e9d0f9 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -105,7 +105,8 @@ import { } from '@theia/core/lib/browser/connection-status-service'; import { BoardsDataMenuUpdater } from './boards/boards-data-menu-updater'; import { BoardsDataStore } from './boards/boards-data-store'; -import { ILogger } from '@theia/core'; +import { ILogger } from '@theia/core/lib/common/logger'; +import { bindContributionProvider } from '@theia/core/lib/common/contribution-provider'; import { FileSystemExt, FileSystemExtPath, @@ -308,7 +309,7 @@ import { CoreErrorHandler } from './contributions/core-error-handler'; import { CompilerErrors } from './contributions/compiler-errors'; import { WidgetManager } from './theia/core/widget-manager'; import { WidgetManager as TheiaWidgetManager } from '@theia/core/lib/browser/widget-manager'; -import { StartupTasks } from './widgets/sketchbook/startup-task'; +import { StartupTasks } from './contributions/startup-task'; import { IndexesUpdateProgress } from './contributions/indexes-update-progress'; import { Daemon } from './contributions/daemon'; import { FirstStartupInstaller } from './contributions/first-startup-installer'; @@ -334,6 +335,8 @@ import { } from './widgets/component-list/filter-renderer'; import { CheckForUpdates } from './contributions/check-for-updates'; import { OutputEditorFactory } from './theia/output/output-editor-factory'; +import { StartupTaskProvider } from '../electron-common/startup-task'; +import { DeleteSketch } from './contributions/delete-sketch'; const registerArduinoThemes = () => { const themes: MonacoThemeJson[] = [ @@ -433,6 +436,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { // Boards service client to receive and delegate notifications from the backend. bind(BoardsServiceProvider).toSelf().inSingletonScope(); bind(FrontendApplicationContribution).toService(BoardsServiceProvider); + bind(CommandContribution).toService(BoardsServiceProvider); // To be able to track, and update the menu based on the core settings (aka. board details) of the currently selected board. bind(FrontendApplicationContribution) @@ -757,6 +761,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { Contribution.configure(bind, OpenBoardsConfig); Contribution.configure(bind, SketchFilesTracker); Contribution.configure(bind, CheckForUpdates); + Contribution.configure(bind, DeleteSketch); + + bindContributionProvider(bind, StartupTaskProvider); + bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window // Disabled the quick-pick customization from Theia when multiple formatters are available. // Use the default VS Code behavior, and pick the first one. In the IDE2, clang-format has `exclusive` selectors. diff --git a/arduino-ide-extension/src/browser/boards/boards-config.tsx b/arduino-ide-extension/src/browser/boards/boards-config.tsx index fb04b0e2e..7edd30e76 100644 --- a/arduino-ide-extension/src/browser/boards/boards-config.tsx +++ b/arduino-ide-extension/src/browser/boards/boards-config.tsx @@ -413,53 +413,5 @@ export namespace BoardsConfig { const { name } = selectedBoard; return `${name}${port ? ` at ${port.address}` : ''}`; } - - export function setConfig( - config: Config | undefined, - urlToAttachTo: URL - ): URL { - const copy = new URL(urlToAttachTo.toString()); - if (!config) { - copy.searchParams.delete('boards-config'); - return copy; - } - - const selectedBoard = config.selectedBoard - ? { - name: config.selectedBoard.name, - fqbn: config.selectedBoard.fqbn, - } - : undefined; - const selectedPort = config.selectedPort - ? { - protocol: config.selectedPort.protocol, - address: config.selectedPort.address, - } - : undefined; - const jsonConfig = JSON.stringify({ selectedBoard, selectedPort }); - copy.searchParams.set('boards-config', encodeURIComponent(jsonConfig)); - return copy; - } - - export function getConfig(url: URL): Config | undefined { - const encoded = url.searchParams.get('boards-config'); - if (!encoded) { - return undefined; - } - try { - const raw = decodeURIComponent(encoded); - const candidate = JSON.parse(raw); - if (typeof candidate === 'object') { - return candidate; - } - console.warn( - `Expected candidate to be an object. It was ${typeof candidate}. URL was: ${url}` - ); - return undefined; - } catch (e) { - console.log(`Could not get board config from URL: ${url}.`, e); - return undefined; - } - } } } diff --git a/arduino-ide-extension/src/browser/boards/boards-service-provider.ts b/arduino-ide-extension/src/browser/boards/boards-service-provider.ts index 0f0fbbea9..25aa8d058 100644 --- a/arduino-ide-extension/src/browser/boards/boards-service-provider.ts +++ b/arduino-ide-extension/src/browser/boards/boards-service-provider.ts @@ -1,7 +1,12 @@ import { injectable, inject } from '@theia/core/shared/inversify'; import { Emitter } from '@theia/core/lib/common/event'; import { ILogger } from '@theia/core/lib/common/logger'; -import { CommandService } from '@theia/core/lib/common/command'; +import { + Command, + CommandContribution, + CommandRegistry, + CommandService, +} from '@theia/core/lib/common/command'; import { MessageService } from '@theia/core/lib/common/message-service'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'; import { RecursiveRequired } from '../../common/types'; @@ -23,9 +28,18 @@ import { nls } from '@theia/core/lib/common'; import { Deferred } from '@theia/core/lib/common/promise-util'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; import { Unknown } from '../../common/nls'; +import { + StartupTask, + StartupTaskProvider, +} from '../../electron-common/startup-task'; @injectable() -export class BoardsServiceProvider implements FrontendApplicationContribution { +export class BoardsServiceProvider + implements + FrontendApplicationContribution, + StartupTaskProvider, + CommandContribution +{ @inject(ILogger) protected logger: ILogger; @@ -50,6 +64,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution { AvailableBoard[] >(); protected readonly onAvailablePortsChangedEmitter = new Emitter(); + private readonly inheritedConfig = new Deferred(); /** * Used for the auto-reconnecting. Sometimes, the attached board gets disconnected after uploading something to it. @@ -115,6 +130,13 @@ export class BoardsServiceProvider implements FrontendApplicationContribution { }); } + registerCommands(registry: CommandRegistry): void { + registry.registerCommand(USE_INHERITED_CONFIG, { + execute: (inheritedConfig: BoardsConfig.Config) => + this.inheritedConfig.resolve(inheritedConfig), + }); + } + get reconciled(): Promise { return this._reconciled.promise; } @@ -655,11 +677,14 @@ export class BoardsServiceProvider implements FrontendApplicationContribution { let storedLatestBoardsConfig = await this.getData< BoardsConfig.Config | undefined >('latest-boards-config'); - // Try to get from the URL if it was not persisted. + // Try to get from the startup task. Wait for it, then timeout. Maybe it never arrives. if (!storedLatestBoardsConfig) { - storedLatestBoardsConfig = BoardsConfig.Config.getConfig( - new URL(window.location.href) - ); + storedLatestBoardsConfig = await Promise.race([ + this.inheritedConfig.promise, + new Promise((resolve) => + setTimeout(() => resolve(undefined), 2_000) + ), + ]); } if (storedLatestBoardsConfig) { this.latestBoardsConfig = storedLatestBoardsConfig; @@ -682,8 +707,31 @@ export class BoardsServiceProvider implements FrontendApplicationContribution { key ); } + + tasks(): StartupTask[] { + return [ + { + command: USE_INHERITED_CONFIG.id, + args: [this.boardsConfig], + }, + ]; + } } +/** + * It should be neither visible nor called from outside. + * + * This service creates a startup task with the current board config and + * passes the task to the electron-main process so that the new window + * can inherit the boards config state of this service. + * + * Note that the state is always set, but new windows might ignore it. + * For example, the new window already has a valid boards config persisted to the local storage. + */ +const USE_INHERITED_CONFIG: Command = { + id: 'arduino-use-inherited-boards-config', +}; + /** * Representation of a ready-to-use board, either the user has configured it or was automatically recognized by the CLI. * An available board was not necessarily recognized by the CLI (e.g.: it is a 3rd party board) or correctly configured but ready for `verify`. diff --git a/arduino-ide-extension/src/browser/contributions/delete-sketch.ts b/arduino-ide-extension/src/browser/contributions/delete-sketch.ts new file mode 100644 index 000000000..c72b77df9 --- /dev/null +++ b/arduino-ide-extension/src/browser/contributions/delete-sketch.ts @@ -0,0 +1,45 @@ +import { injectable } from '@theia/core/shared/inversify'; +import { SketchesError } from '../../common/protocol'; +import { + Command, + CommandRegistry, + SketchContribution, + Sketch, +} from './contribution'; + +@injectable() +export class DeleteSketch extends SketchContribution { + override registerCommands(registry: CommandRegistry): void { + registry.registerCommand(DeleteSketch.Commands.DELETE_SKETCH, { + execute: (uri: string) => this.deleteSketch(uri), + }); + } + + private async deleteSketch(uri: string): Promise { + const sketch = await this.loadSketch(uri); + if (!sketch) { + console.info(`Sketch not found at ${uri}. Skipping deletion.`); + return; + } + return this.sketchService.deleteSketch(sketch); + } + + private async loadSketch(uri: string): Promise { + try { + const sketch = await this.sketchService.loadSketch(uri); + return sketch; + } catch (err) { + if (SketchesError.NotFound.is(err)) { + return undefined; + } + throw err; + } + } +} +export namespace DeleteSketch { + export namespace Commands { + export const DELETE_SKETCH: Command = { + id: 'arduino-delete-sketch', + }; + } +} diff --git a/arduino-ide-extension/src/browser/contributions/save-as-sketch.ts b/arduino-ide-extension/src/browser/contributions/save-as-sketch.ts index 2954a6038..6898add2a 100644 --- a/arduino-ide-extension/src/browser/contributions/save-as-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/save-as-sketch.ts @@ -12,21 +12,19 @@ import { } from './contribution'; import { nls } from '@theia/core/lib/common'; import { ApplicationShell, NavigatableWidget, Saveable } from '@theia/core/lib/browser'; -import { EditorManager } from '@theia/editor/lib/browser'; import { WindowService } from '@theia/core/lib/browser/window/window-service'; import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl'; +import { WorkspaceInput } from '@theia/workspace/lib/browser'; +import { StartupTask } from '../../electron-common/startup-task'; +import { DeleteSketch } from './delete-sketch'; @injectable() export class SaveAsSketch extends SketchContribution { - @inject(ApplicationShell) - protected readonly applicationShell: ApplicationShell; - - @inject(EditorManager) - protected override readonly editorManager: EditorManager; + private readonly applicationShell: ApplicationShell; @inject(WindowService) - protected readonly windowService: WindowService; + private readonly windowService: WindowService; override registerCommands(registry: CommandRegistry): void { registry.registerCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH, { @@ -107,21 +105,19 @@ export class SaveAsSketch extends SketchContribution { this.sketchService.markAsRecentlyOpened(workspaceUri); } } + const options: WorkspaceInput & StartupTask.Owner = { + preserveWindow: true, + tasks: [], + }; if (workspaceUri && openAfterMove) { this.windowService.setSafeToShutDown(); if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) { - // This window will navigate away. - // Explicitly stop the contribution to dispose the file watcher before deleting the temp sketch. - // Otherwise, users might see irrelevant _Unable to watch for file changes in this large workspace._ notification. - // https://github.com/arduino/arduino-ide/issues/39. - this.sketchServiceClient.onStop(); - // TODO: consider implementing the temp sketch deletion the following way: - // Open the other sketch with a `delete the temp sketch` startup-task. - this.sketchService.notifyDeleteSketch(sketch); // This is a notification and will execute on the backend. + options.tasks.push({ + command: DeleteSketch.Commands.DELETE_SKETCH.id, + args: [sketch.uri], + }); } - this.workspaceService.open(new URI(workspaceUri), { - preserveWindow: true, - }); + this.workspaceService.open(new URI(workspaceUri), options); } return !!workspaceUri; } diff --git a/arduino-ide-extension/src/browser/contributions/sketch-files-tracker.ts b/arduino-ide-extension/src/browser/contributions/sketch-files-tracker.ts index fcd07cb67..3c7daea48 100644 --- a/arduino-ide-extension/src/browser/contributions/sketch-files-tracker.ts +++ b/arduino-ide-extension/src/browser/contributions/sketch-files-tracker.ts @@ -4,7 +4,7 @@ import { inject, injectable } from '@theia/core/shared/inversify'; import { FileSystemFrontendContribution } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution'; import { FileChangeType } from '@theia/filesystem/lib/common/files'; import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl'; -import { Sketch, SketchContribution, URI } from './contribution'; +import { Sketch, SketchContribution } from './contribution'; import { OpenSketchFiles } from './open-sketch-files'; @injectable() @@ -31,7 +31,6 @@ export class SketchFilesTracker extends SketchContribution { override onReady(): void { this.sketchServiceClient.currentSketch().then(async (sketch) => { if (CurrentSketch.isValid(sketch)) { - this.toDisposeOnStop.push(this.fileService.watch(new URI(sketch.uri))); this.toDisposeOnStop.push( this.fileService.onDidFilesChange(async (event) => { for (const { type, resource } of event.changes) { diff --git a/arduino-ide-extension/src/browser/contributions/startup-task.ts b/arduino-ide-extension/src/browser/contributions/startup-task.ts new file mode 100644 index 000000000..b37b558ea --- /dev/null +++ b/arduino-ide-extension/src/browser/contributions/startup-task.ts @@ -0,0 +1,52 @@ +import * as remote from '@theia/core/electron-shared/@electron/remote'; +import type { IpcRendererEvent } from '@theia/core/electron-shared/electron'; +import { ipcRenderer } from '@theia/core/electron-shared/electron'; +import { injectable } from '@theia/core/shared/inversify'; +import { StartupTask } from '../../electron-common/startup-task'; +import { Contribution } from './contribution'; + +@injectable() +export class StartupTasks extends Contribution { + override onReady(): void { + ipcRenderer.once( + StartupTask.Messaging.STARTUP_TASKS_SIGNAL, + (_: IpcRendererEvent, args: unknown) => { + console.debug( + `Received the startup tasks from the electron main process. Args: ${JSON.stringify( + args + )}` + ); + if (!StartupTask.has(args)) { + console.warn(`Could not detect 'tasks' from the signal. Skipping.`); + return; + } + const tasks = args.tasks; + if (tasks.length) { + console.log(`Executing startup tasks:`); + tasks.forEach(({ command, args = [] }) => { + console.log( + ` - '${command}' ${ + args.length ? `, args: ${JSON.stringify(args)}` : '' + }` + ); + this.commandService + .executeCommand(command, ...args) + .catch((err) => + console.error( + `Error occurred when executing the startup task '${command}'${ + args?.length ? ` with args: '${JSON.stringify(args)}` : '' + }.`, + err + ) + ); + }); + } + } + ); + const { id } = remote.getCurrentWindow(); + console.debug( + `Signalling app ready event to the electron main process. Sender ID: ${id}.` + ); + ipcRenderer.send(StartupTask.Messaging.APP_READY_SIGNAL(id)); + } +} diff --git a/arduino-ide-extension/src/browser/theia/core/browser-main-menu-factory.ts b/arduino-ide-extension/src/browser/theia/core/browser-main-menu-factory.ts deleted file mode 100644 index 737ca98ac..000000000 --- a/arduino-ide-extension/src/browser/theia/core/browser-main-menu-factory.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { injectable } from '@theia/core/shared/inversify'; -import { - BrowserMainMenuFactory as TheiaBrowserMainMenuFactory, - MenuBarWidget, -} from '@theia/core/lib/browser/menu/browser-menu-plugin'; -import { MainMenuManager } from '../../../common/main-menu-manager'; - -@injectable() -export class BrowserMainMenuFactory - extends TheiaBrowserMainMenuFactory - implements MainMenuManager -{ - protected menuBar: MenuBarWidget | undefined; - - override createMenuBar(): MenuBarWidget { - this.menuBar = super.createMenuBar(); - return this.menuBar; - } - - update(): void { - if (this.menuBar) { - this.menuBar.clearMenus(); - this.fillMenuBar(this.menuBar); - } - } -} diff --git a/arduino-ide-extension/src/browser/theia/core/browser-menu-module.ts b/arduino-ide-extension/src/browser/theia/core/browser-menu-module.ts deleted file mode 100644 index fb887142f..000000000 --- a/arduino-ide-extension/src/browser/theia/core/browser-menu-module.ts +++ /dev/null @@ -1,18 +0,0 @@ -import '../../../../src/browser/style/browser-menu.css'; -import { ContainerModule } from '@theia/core/shared/inversify'; -import { - BrowserMenuBarContribution, - BrowserMainMenuFactory as TheiaBrowserMainMenuFactory, -} from '@theia/core/lib/browser/menu/browser-menu-plugin'; -import { MainMenuManager } from '../../../common/main-menu-manager'; -import { ArduinoMenuContribution } from './browser-menu-plugin'; -import { BrowserMainMenuFactory } from './browser-main-menu-factory'; - -export default new ContainerModule((bind, unbind, isBound, rebind) => { - bind(BrowserMainMenuFactory).toSelf().inSingletonScope(); - bind(MainMenuManager).toService(BrowserMainMenuFactory); - rebind(TheiaBrowserMainMenuFactory).toService(BrowserMainMenuFactory); - rebind(BrowserMenuBarContribution) - .to(ArduinoMenuContribution) - .inSingletonScope(); -}); diff --git a/arduino-ide-extension/src/browser/theia/core/browser-window-module.ts b/arduino-ide-extension/src/browser/theia/core/browser-window-module.ts deleted file mode 100644 index ac0862b75..000000000 --- a/arduino-ide-extension/src/browser/theia/core/browser-window-module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { DefaultWindowService as TheiaDefaultWindowService } from '@theia/core/lib/browser/window/default-window-service'; -import { ContainerModule } from '@theia/core/shared/inversify'; -import { DefaultWindowService } from './default-window-service'; -import { WindowServiceExt } from './window-service-ext'; - -export default new ContainerModule((bind, unbind, isBound, rebind) => { - bind(DefaultWindowService).toSelf().inSingletonScope(); - rebind(TheiaDefaultWindowService).toService(DefaultWindowService); - bind(WindowServiceExt).toService(DefaultWindowService); -}); diff --git a/arduino-ide-extension/src/browser/theia/core/default-window-service.ts b/arduino-ide-extension/src/browser/theia/core/default-window-service.ts deleted file mode 100644 index 145691727..000000000 --- a/arduino-ide-extension/src/browser/theia/core/default-window-service.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { DefaultWindowService as TheiaDefaultWindowService } from '@theia/core/lib/browser/window/default-window-service'; -import { injectable } from '@theia/core/shared/inversify'; -import { WindowServiceExt } from './window-service-ext'; - -@injectable() -export class DefaultWindowService - extends TheiaDefaultWindowService - implements WindowServiceExt -{ - /** - * The default implementation always resolves to `true`. - * IDE2 does not use it. It's currently an electron-only app. - */ - async isFirstWindow(): Promise { - return true; - } -} diff --git a/arduino-ide-extension/src/browser/theia/core/window-service-ext.ts b/arduino-ide-extension/src/browser/theia/core/window-service-ext.ts index f22e55cc4..a34f6882b 100644 --- a/arduino-ide-extension/src/browser/theia/core/window-service-ext.ts +++ b/arduino-ide-extension/src/browser/theia/core/window-service-ext.ts @@ -1,7 +1,10 @@ +import type { StartupTask } from '../../../electron-common/startup-task'; + export const WindowServiceExt = Symbol('WindowServiceExt'); export interface WindowServiceExt { /** * Returns with a promise that resolves to `true` if the current window is the first window. */ isFirstWindow(): Promise; + reload(options?: StartupTask.Owner): void; } diff --git a/arduino-ide-extension/src/browser/theia/workspace/workspace-service.ts b/arduino-ide-extension/src/browser/theia/workspace/workspace-service.ts index 38f15c9d1..e9d1e1e69 100644 --- a/arduino-ide-extension/src/browser/theia/workspace/workspace-service.ts +++ b/arduino-ide-extension/src/browser/theia/workspace/workspace-service.ts @@ -1,54 +1,41 @@ import * as remote from '@theia/core/electron-shared/@electron/remote'; -import { injectable, inject } from '@theia/core/shared/inversify'; +import { injectable, inject, named } from '@theia/core/shared/inversify'; import URI from '@theia/core/lib/common/uri'; import { EditorWidget } from '@theia/editor/lib/browser'; -import { LabelProvider } from '@theia/core/lib/browser/label-provider'; -import { MessageService } from '@theia/core/lib/common/message-service'; import { ApplicationServer } from '@theia/core/lib/common/application-protocol'; import { FrontendApplication } from '@theia/core/lib/browser/frontend-application'; import { FocusTracker, Widget } from '@theia/core/lib/browser'; -import { DEFAULT_WINDOW_HASH } from '@theia/core/lib/common/window'; -import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; +import { + DEFAULT_WINDOW_HASH, + NewWindowOptions, +} from '@theia/core/lib/common/window'; import { WorkspaceInput, WorkspaceService as TheiaWorkspaceService, } from '@theia/workspace/lib/browser/workspace-service'; -import { ConfigService } from '../../../common/protocol/config-service'; import { SketchesService, Sketch, } from '../../../common/protocol/sketches-service'; -import { BoardsServiceProvider } from '../../boards/boards-service-provider'; -import { BoardsConfig } from '../../boards/boards-config'; import { FileStat } from '@theia/filesystem/lib/common/files'; import { StartupTask, - StartupTasks, -} from '../../widgets/sketchbook/startup-task'; -import { setURL } from '../../utils/window'; + StartupTaskProvider, +} from '../../../electron-common/startup-task'; +import { WindowServiceExt } from '../core/window-service-ext'; +import { ContributionProvider } from '@theia/core/lib/common/contribution-provider'; @injectable() export class WorkspaceService extends TheiaWorkspaceService { @inject(SketchesService) - protected readonly sketchService: SketchesService; - - @inject(ConfigService) - protected readonly configService: ConfigService; - - @inject(LabelProvider) - protected override readonly labelProvider: LabelProvider; - - @inject(MessageService) - protected override readonly messageService: MessageService; - + private readonly sketchService: SketchesService; @inject(ApplicationServer) - protected readonly applicationServer: ApplicationServer; - - @inject(FrontendApplicationStateService) - protected readonly appStateService: FrontendApplicationStateService; - - @inject(BoardsServiceProvider) - protected readonly boardsServiceProvider: BoardsServiceProvider; + private readonly applicationServer: ApplicationServer; + @inject(WindowServiceExt) + private readonly windowServiceExt: WindowServiceExt; + @inject(ContributionProvider) + @named(StartupTaskProvider) + private readonly providers: ContributionProvider; private version?: string; @@ -156,27 +143,33 @@ export class WorkspaceService extends TheiaWorkspaceService { } protected override reloadWindow(options?: WorkspaceInput): void { - if (StartupTasks.WorkspaceInput.is(options)) { - setURL(StartupTask.append(options.tasks, new URL(window.location.href))); - } - super.reloadWindow(); + const tasks = this.tasks(options); + this.setURLFragment(this._workspace?.resource.path.toString() || ''); + this.windowServiceExt.reload({ tasks }); } protected override openNewWindow( workspacePath: string, options?: WorkspaceInput ): void { - const { boardsConfig } = this.boardsServiceProvider; - let url = BoardsConfig.Config.setConfig( - boardsConfig, - new URL(window.location.href) - ); // Set the current boards config for the new browser window. - url.hash = workspacePath; - if (StartupTasks.WorkspaceInput.is(options)) { - url = StartupTask.append(options.tasks, url); - } + const tasks = this.tasks(options); + const url = new URL(window.location.href); + url.hash = encodeURI(workspacePath); + this.windowService.openNewWindow( + url.toString(), + Object.assign({} as NewWindowOptions, { tasks }) + ); + } - this.windowService.openNewWindow(url.toString()); + private tasks(options?: WorkspaceInput): StartupTask[] { + const tasks = this.providers + .getContributions() + .map((contribution) => contribution.tasks()) + .reduce((prev, curr) => prev.concat(curr), []); + if (StartupTask.has(options)) { + tasks.push(...options.tasks); + } + return tasks; } protected onCurrentWidgetChange({ diff --git a/arduino-ide-extension/src/browser/widgets/sketchbook/startup-task.ts b/arduino-ide-extension/src/browser/widgets/sketchbook/startup-task.ts deleted file mode 100644 index 25cf33a3e..000000000 --- a/arduino-ide-extension/src/browser/widgets/sketchbook/startup-task.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { injectable } from '@theia/core/shared/inversify'; -import { WorkspaceInput as TheiaWorkspaceInput } from '@theia/workspace/lib/browser'; -import { Contribution } from '../../contributions/contribution'; -import { setURL } from '../../utils/window'; - -@injectable() -export class StartupTasks extends Contribution { - override onReady(): void { - const tasks = StartupTask.get(new URL(window.location.href)); - console.log(`Executing startup tasks: ${JSON.stringify(tasks)}`); - tasks.forEach(({ command, args = [] }) => - this.commandService - .executeCommand(command, ...args) - .catch((err) => - console.error( - `Error occurred when executing the startup task '${command}'${ - args?.length ? ` with args: '${JSON.stringify(args)}` : '' - }.`, - err - ) - ) - ); - if (tasks.length) { - // Remove the startup tasks after the execution. - // Otherwise, IDE2 executes them again on a window reload event. - setURL(StartupTask.set([], new URL(window.location.href))); - console.info(`Removed startup tasks from URL.`); - } - } -} - -export interface StartupTask { - command: string; - /** - * Must be JSON serializable. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - args?: any[]; -} -export namespace StartupTask { - const QUERY = 'startupTasks'; - export function is(arg: unknown): arg is StartupTasks { - if (typeof arg === 'object') { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const object = arg as any; - return 'command' in object && typeof object['command'] === 'string'; - } - return false; - } - export function get(url: URL): StartupTask[] { - const { searchParams } = url; - const encodedTasks = searchParams.get(QUERY); - if (encodedTasks) { - const rawTasks = decodeURIComponent(encodedTasks); - const tasks = JSON.parse(rawTasks); - if (Array.isArray(tasks)) { - return tasks.filter((task) => { - if (StartupTask.is(task)) { - return true; - } - console.warn(`Was not a task: ${JSON.stringify(task)}. Ignoring.`); - return false; - }); - } else { - debugger; - console.warn(`Startup tasks was not an array: ${rawTasks}. Ignoring.`); - } - } - return []; - } - export function set(tasks: StartupTask[], url: URL): URL { - const copy = new URL(url); - copy.searchParams.set(QUERY, encodeURIComponent(JSON.stringify(tasks))); - return copy; - } - export function append(tasks: StartupTask[], url: URL): URL { - return set([...get(url), ...tasks], url); - } -} - -export namespace StartupTasks { - export interface WorkspaceInput extends TheiaWorkspaceInput { - tasks: StartupTask[]; - } - export namespace WorkspaceInput { - export function is( - input: (TheiaWorkspaceInput & Partial) | undefined - ): input is WorkspaceInput { - return !!input && !!input.tasks; - } - } -} diff --git a/arduino-ide-extension/src/common/protocol/sketches-service-client-impl.ts b/arduino-ide-extension/src/common/protocol/sketches-service-client-impl.ts index 34da46bfd..595761be7 100644 --- a/arduino-ide-extension/src/common/protocol/sketches-service-client-impl.ts +++ b/arduino-ide-extension/src/common/protocol/sketches-service-client-impl.ts @@ -165,15 +165,6 @@ export class SketchesServiceClientImpl .reachedState('started_contributions') .then(async () => { const currentSketch = await this.loadCurrentSketch(); - if (CurrentSketch.isValid(currentSketch)) { - this.toDispose.pushAll([ - // Watch the file changes of the current sketch - this.fileService.watch(new URI(currentSketch.uri), { - recursive: true, - excludes: [], - }), - ]); - } this.useCurrentSketch(currentSketch); }); } diff --git a/arduino-ide-extension/src/common/protocol/sketches-service.ts b/arduino-ide-extension/src/common/protocol/sketches-service.ts index 719ecaacd..dbb7c2654 100644 --- a/arduino-ide-extension/src/common/protocol/sketches-service.ts +++ b/arduino-ide-extension/src/common/protocol/sketches-service.ts @@ -97,9 +97,9 @@ export interface SketchesService { getIdeTempFolderUri(sketch: Sketch): Promise; /** - * Notifies the backend to recursively delete the sketch folder with all its content. + * Recursively deletes the sketch folder with all its content. */ - notifyDeleteSketch(sketch: Sketch): void; + deleteSketch(sketch: Sketch): Promise; } export interface SketchRef { diff --git a/arduino-ide-extension/src/electron-browser/theia/core/electron-window-service.ts b/arduino-ide-extension/src/electron-browser/theia/core/electron-window-service.ts index d0d0a1ef2..74629f297 100644 --- a/arduino-ide-extension/src/electron-browser/theia/core/electron-window-service.ts +++ b/arduino-ide-extension/src/electron-browser/theia/core/electron-window-service.ts @@ -1,10 +1,14 @@ import * as remote from '@theia/core/electron-shared/@electron/remote'; +import { ipcRenderer } from '@theia/core/electron-shared/electron'; import { ConnectionStatus, ConnectionStatusService, } from '@theia/core/lib/browser/connection-status-service'; import { nls } from '@theia/core/lib/common'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import { NewWindowOptions } from '@theia/core/lib/common/window'; import { ElectronWindowService as TheiaElectronWindowService } from '@theia/core/lib/electron-browser/window/electron-window-service'; +import { RELOAD_REQUESTED_SIGNAL } from '@theia/core/lib/electron-common/messaging/electron-messages'; import { inject, injectable, @@ -12,6 +16,7 @@ import { } from '@theia/core/shared/inversify'; import { WindowServiceExt } from '../../../browser/theia/core/window-service-ext'; import { ElectronMainWindowServiceExt } from '../../../electron-common/electron-main-window-service-ext'; +import { StartupTask } from '../../../electron-common/startup-task'; @injectable() export class ElectronWindowService @@ -60,14 +65,30 @@ export class ElectronWindowService return response === 0; // 'Yes', close the window. } - private _firstWindow: boolean | undefined; + private _firstWindow: Deferred | undefined; async isFirstWindow(): Promise { if (this._firstWindow === undefined) { + this._firstWindow = new Deferred(); const windowId = remote.getCurrentWindow().id; // This is expensive and synchronous so we check it once per FE. - this._firstWindow = await this.mainWindowServiceExt.isFirstWindow( - windowId - ); + this.mainWindowServiceExt + .isFirstWindow(windowId) + .then((firstWindow) => this._firstWindow?.resolve(firstWindow)); + } + return this._firstWindow.promise; + } + + // Overridden because the default Theia implementation destroys the additional properties of the `options` arg, such as `tasks`. + override openNewWindow(url: string, options?: NewWindowOptions): undefined { + return this.delegate.openNewWindow(url, options); + } + + // Overridden to support optional task owner params and make `tsc` happy. + override reload(options?: StartupTask.Owner): void { + if (options?.tasks && options.tasks.length) { + const { tasks } = options; + ipcRenderer.send(RELOAD_REQUESTED_SIGNAL, { tasks }); + } else { + ipcRenderer.send(RELOAD_REQUESTED_SIGNAL); } - return this._firstWindow; } } diff --git a/arduino-ide-extension/src/electron-common/startup-task.ts b/arduino-ide-extension/src/electron-common/startup-task.ts new file mode 100644 index 000000000..1bde3673d --- /dev/null +++ b/arduino-ide-extension/src/electron-common/startup-task.ts @@ -0,0 +1,50 @@ +export interface StartupTask { + command: string; + /** + * Must be JSON serializable. + * See the restrictions [here](https://www.electronjs.org/docs/latest/api/web-contents#contentssendchannel-args). + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + args?: any[]; +} +export namespace StartupTask { + export function is(arg: unknown): arg is StartupTask { + if (typeof arg === 'object') { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const object = arg as any; + return ( + 'command' in object && + typeof object['command'] === 'string' && + (!('args' in object) || Array.isArray(object['args'])) + ); + } + return false; + } + export function has(arg: unknown): arg is unknown & Owner { + if (typeof arg === 'object') { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const object = arg as any; + return ( + 'tasks' in object && + Array.isArray(object['tasks']) && + object['tasks'].every(is) + ); + } + return false; + } + export namespace Messaging { + export const STARTUP_TASKS_SIGNAL = 'arduino/startupTasks'; + export function APP_READY_SIGNAL(id: number): string { + return `arduino/appReady${id}`; + } + } + + export interface Owner { + readonly tasks: StartupTask[]; + } +} + +export const StartupTaskProvider = Symbol('StartupTaskProvider'); +export interface StartupTaskProvider { + tasks(): StartupTask[]; +} diff --git a/arduino-ide-extension/src/electron-main/arduino-electron-main-module.ts b/arduino-ide-extension/src/electron-main/arduino-electron-main-module.ts index b6767e57d..9ab5bc99d 100644 --- a/arduino-ide-extension/src/electron-main/arduino-electron-main-module.ts +++ b/arduino-ide-extension/src/electron-main/arduino-electron-main-module.ts @@ -12,12 +12,8 @@ import { IDEUpdaterClient, IDEUpdaterPath, } from '../common/protocol/ide-updater'; -import { - ElectronMainWindowServiceExt, - electronMainWindowServiceExtPath, -} from '../electron-common/electron-main-window-service-ext'; +import { electronMainWindowServiceExtPath } from '../electron-common/electron-main-window-service-ext'; import { IsTempSketch } from '../node/is-temp-sketch'; -import { ElectronMainWindowServiceExtImpl } from './electron-main-window-service-ext-impl'; import { IDEUpdaterImpl } from './ide-updater/ide-updater-impl'; import { ElectronMainApplication } from './theia/electron-main-application'; import { ElectronMainWindowServiceImpl } from './theia/electron-main-window-service'; @@ -52,14 +48,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(TheiaElectronWindow).toSelf(); rebind(DefaultTheiaElectronWindow).toService(TheiaElectronWindow); - bind(ElectronMainWindowServiceExt) - .to(ElectronMainWindowServiceExtImpl) - .inSingletonScope(); bind(ElectronConnectionHandler) .toDynamicValue( (context) => new JsonRpcConnectionHandler(electronMainWindowServiceExtPath, () => - context.container.get(ElectronMainWindowServiceExt) + context.container.get(ElectronMainWindowServiceImpl) ) ) .inSingletonScope(); diff --git a/arduino-ide-extension/src/electron-main/electron-main-window-service-ext-impl.ts b/arduino-ide-extension/src/electron-main/electron-main-window-service-ext-impl.ts deleted file mode 100644 index a0c872786..000000000 --- a/arduino-ide-extension/src/electron-main/electron-main-window-service-ext-impl.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { inject, injectable } from '@theia/core/shared/inversify'; -import { ElectronMainWindowServiceExt } from '../electron-common/electron-main-window-service-ext'; -import { ElectronMainApplication } from './theia/electron-main-application'; - -@injectable() -export class ElectronMainWindowServiceExtImpl - implements ElectronMainWindowServiceExt -{ - @inject(ElectronMainApplication) - private readonly app: ElectronMainApplication; - - async isFirstWindow(windowId: number): Promise { - return this.app.firstWindowId === windowId; - } -} diff --git a/arduino-ide-extension/src/electron-main/theia/electron-main-window-service.ts b/arduino-ide-extension/src/electron-main/theia/electron-main-window-service.ts index 2248ac543..d26055dec 100644 --- a/arduino-ide-extension/src/electron-main/theia/electron-main-window-service.ts +++ b/arduino-ide-extension/src/electron-main/theia/electron-main-window-service.ts @@ -1,34 +1,63 @@ -import { inject, injectable } from '@theia/core/shared/inversify'; +import type { NewWindowOptions } from '@theia/core/lib/common/window'; +import type { BrowserWindow } from '@theia/core/electron-shared/electron'; import { ElectronMainWindowServiceImpl as TheiaElectronMainWindowService } from '@theia/core/lib/electron-main/electron-main-window-service-impl'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { ElectronMainWindowServiceExt } from '../../electron-common/electron-main-window-service-ext'; +import { StartupTask } from '../../electron-common/startup-task'; import { ElectronMainApplication } from './electron-main-application'; -import { NewWindowOptions } from '@theia/core/lib/common/window'; +import { load } from './window'; @injectable() -export class ElectronMainWindowServiceImpl extends TheiaElectronMainWindowService { +export class ElectronMainWindowServiceImpl + extends TheiaElectronMainWindowService + implements ElectronMainWindowServiceExt +{ @inject(ElectronMainApplication) protected override readonly app: ElectronMainApplication; - override openNewWindow(url: string, { external }: NewWindowOptions): undefined { - if (!external) { - const sanitizedUrl = this.sanitize(url); - const existing = this.app.browserWindows.find( - (window) => this.sanitize(window.webContents.getURL()) === sanitizedUrl - ); - if (existing) { - existing.focus(); - return; - } - } - return super.openNewWindow(url, { external }); + async isFirstWindow(windowId: number): Promise { + return this.app.firstWindowId === windowId; } - private sanitize(url: string): string { - const copy = new URL(url); - const searchParams: string[] = []; - copy.searchParams.forEach((_, key) => searchParams.push(key)); - for (const param of searchParams) { - copy.searchParams.delete(param); + override openNewWindow(url: string, options: NewWindowOptions): undefined { + // External window has highest precedence. + if (options?.external) { + return super.openNewWindow(url, options); + } + + // Look for existing window with the same URL and focus it. + const existing = this.app.browserWindows.find( + ({ webContents }) => webContents.getURL() === url + ); + if (existing) { + existing.focus(); + return undefined; } - return copy.toString(); + + // Create new window and share the startup tasks. + if (StartupTask.has(options)) { + const { tasks } = options; + this.app.createWindow().then((electronWindow) => { + this.loadURL(electronWindow, url).then(() => { + electronWindow.webContents.send( + StartupTask.Messaging.STARTUP_TASKS_SIGNAL, + { tasks } + ); + }); + }); + return undefined; + } + + // Default. + return super.openNewWindow(url, options); + } + + private loadURL( + electronWindow: BrowserWindow, + url: string + ): Promise { + return load(electronWindow, (electronWindow) => + electronWindow.loadURL(url) + ); } } diff --git a/arduino-ide-extension/src/electron-main/theia/theia-electron-window.ts b/arduino-ide-extension/src/electron-main/theia/theia-electron-window.ts index 8c5fd03bd..74dee4e90 100644 --- a/arduino-ide-extension/src/electron-main/theia/theia-electron-window.ts +++ b/arduino-ide-extension/src/electron-main/theia/theia-electron-window.ts @@ -1,12 +1,15 @@ import { injectable } from '@theia/core/shared/inversify'; import { ipcMain, IpcMainEvent } from '@theia/electron/shared/electron'; -import { StopReason } from '@theia/core/lib/electron-common/messaging/electron-messages'; +import { + RELOAD_REQUESTED_SIGNAL, + StopReason, +} from '@theia/core/lib/electron-common/messaging/electron-messages'; import { TheiaElectronWindow as DefaultTheiaElectronWindow } from '@theia/core/lib/electron-main/theia-electron-window'; import { FileUri } from '@theia/core/lib/node'; import URI from '@theia/core/lib/common/uri'; -import { FrontendApplicationState } from '@theia/core/lib/common/frontend-application-state'; import { createDisposableListener } from '@theia/core/lib/electron-main/event-utils'; -import { APPLICATION_STATE_CHANGE_SIGNAL } from '@theia/core/lib/electron-common/messaging/electron-messages'; +import { StartupTask } from '../../electron-common/startup-task'; +import { load } from './window'; @injectable() export class TheiaElectronWindow extends DefaultTheiaElectronWindow { @@ -38,30 +41,42 @@ export class TheiaElectronWindow extends DefaultTheiaElectronWindow { return false; } - // Note: does the same as the Theia impl, but logs state changes. - protected override trackApplicationState(): void { + protected override reload(tasks?: StartupTask[]): void { + this.handleStopRequest(() => { + this.applicationState = 'init'; + if (tasks && tasks.length) { + load(this._window, (electronWindow) => electronWindow.reload()).then( + (electronWindow) => + electronWindow.webContents.send( + StartupTask.Messaging.STARTUP_TASKS_SIGNAL, + { tasks } + ) + ); + } else { + this._window.reload(); + } + }, StopReason.Reload); + } + + protected override attachReloadListener(): void { createDisposableListener( ipcMain, - APPLICATION_STATE_CHANGE_SIGNAL, - (e: IpcMainEvent, state: FrontendApplicationState) => { - console.log( - 'app-state-change', - `>>> new app state <${state} was received from sender <${e.sender.id}>. current window ID: ${this._window.id}` - ); + RELOAD_REQUESTED_SIGNAL, + (e: IpcMainEvent, arg: unknown) => { if (this.isSender(e)) { - this.applicationState = state; - console.log( - 'app-state-change', - `<<< new app state is <${this.applicationState}> for window <${this._window.id}>` - ); - } else { - console.log( - 'app-state-change', - `<<< new app state <${state}> is ignored from <${e.sender.id}>. current window ID is <${this._window.id}>` - ); + if (StartupTask.has(arg)) { + this.reload(arg.tasks); + } else { + this.reload(); + } } }, this.toDispose ); } + + // https://github.com/eclipse-theia/theia/issues/11600#issuecomment-1240657481 + protected override isSender(e: IpcMainEvent): boolean { + return e.sender.id === this._window.webContents.id; + } } diff --git a/arduino-ide-extension/src/electron-main/theia/window.ts b/arduino-ide-extension/src/electron-main/theia/window.ts new file mode 100644 index 000000000..8eac41f51 --- /dev/null +++ b/arduino-ide-extension/src/electron-main/theia/window.ts @@ -0,0 +1,33 @@ +import { MaybePromise } from '@theia/core'; +import type { IpcMainEvent } from '@theia/core/electron-shared/electron'; +import { BrowserWindow, ipcMain } from '@theia/core/electron-shared/electron'; +import { DisposableCollection } from '@theia/core/lib/common/disposable'; +import { createDisposableListener } from '@theia/core/lib/electron-main/event-utils'; +import { StartupTask } from '../../electron-common/startup-task'; + +/** + * Should be used to load (URL) or reload a window. The returning promise will resolve + * when the app is ready to receive startup tasks. + */ +export async function load( + electronWindow: BrowserWindow, + doLoad: (electronWindow: BrowserWindow) => MaybePromise +): Promise { + const { id } = electronWindow; + const toDispose = new DisposableCollection(); + const channel = StartupTask.Messaging.APP_READY_SIGNAL(id); + return new Promise((resolve, reject) => { + toDispose.push( + createDisposableListener( + ipcMain, + channel, + ({ sender: webContents }: IpcMainEvent) => { + if (webContents.id === electronWindow.webContents.id) { + resolve(electronWindow); + } + } + ) + ); + Promise.resolve(doLoad(electronWindow)).catch(reject); + }).finally(() => toDispose.dispose()); +} diff --git a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts index b007b1bc3..b2486e478 100644 --- a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts +++ b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts @@ -1,4 +1,4 @@ -import { ContainerModule } from '@theia/core/shared/inversify'; +import { ContainerModule, interfaces } from '@theia/core/shared/inversify'; import { ArduinoDaemonImpl } from './arduino-daemon-impl'; import { ArduinoFirmwareUploader, @@ -110,6 +110,7 @@ import { SurveyNotificationServicePath, } from '../common/protocol/survey-service'; import { IsTempSketch } from './is-temp-sketch'; +import { rebindNsfwFileSystemWatcher } from './theia/filesystem/nsfw-watcher/nsfw-bindings'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(BackendApplication).toSelf().inSingletonScope(); @@ -288,6 +289,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { ) ) .inSingletonScope(); + rebindNsfwFileSystemWatcher(rebind); // Output service per connection. bind(ConnectionContainerModule).toConstantValue( @@ -325,58 +327,14 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { }) ); - // Logger for the Arduino daemon - bind(ILogger) - .toDynamicValue((ctx) => { - const parentLogger = ctx.container.get(ILogger); - return parentLogger.child('daemon'); - }) - .inSingletonScope() - .whenTargetNamed('daemon'); - - // Logger for the Arduino daemon - bind(ILogger) - .toDynamicValue((ctx) => { - const parentLogger = ctx.container.get(ILogger); - return parentLogger.child('fwuploader'); - }) - .inSingletonScope() - .whenTargetNamed('fwuploader'); - - // Logger for the "serial discovery". - bind(ILogger) - .toDynamicValue((ctx) => { - const parentLogger = ctx.container.get(ILogger); - return parentLogger.child('discovery-log'); // TODO: revert - }) - .inSingletonScope() - .whenTargetNamed('discovery-log'); // TODO: revert - - // Logger for the CLI config service. From the CLI config (FS path aware), we make a URI-aware app config. - bind(ILogger) - .toDynamicValue((ctx) => { - const parentLogger = ctx.container.get(ILogger); - return parentLogger.child('config'); - }) - .inSingletonScope() - .whenTargetNamed('config'); - - // Logger for the monitor manager and its services - bind(ILogger) - .toDynamicValue((ctx) => { - const parentLogger = ctx.container.get(ILogger); - return parentLogger.child(MonitorManagerName); - }) - .inSingletonScope() - .whenTargetNamed(MonitorManagerName); - - bind(ILogger) - .toDynamicValue((ctx) => { - const parentLogger = ctx.container.get(ILogger); - return parentLogger.child(MonitorServiceName); - }) - .inSingletonScope() - .whenTargetNamed(MonitorServiceName); + [ + 'daemon', // Logger for the Arduino daemon + 'fwuploader', // Arduino Firmware uploader + 'discovery-log', // Boards discovery + 'config', // Logger for the CLI config reading and manipulation + MonitorManagerName, // Logger for the monitor manager and its services + MonitorServiceName, + ].forEach((name) => bindChildLogger(bind, name)); // Remote sketchbook bindings bind(AuthenticationServiceImpl).toSelf().inSingletonScope(); @@ -423,3 +381,12 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(IsTempSketch).toSelf().inSingletonScope(); }); + +function bindChildLogger(bind: interfaces.Bind, name: string): void { + bind(ILogger) + .toDynamicValue(({ container }) => + container.get(ILogger).child(name) + ) + .inSingletonScope() + .whenTargetNamed(name); +} diff --git a/arduino-ide-extension/src/node/sketches-service-impl.ts b/arduino-ide-extension/src/node/sketches-service-impl.ts index bba68f941..0912d4733 100644 --- a/arduino-ide-extension/src/node/sketches-service-impl.ts +++ b/arduino-ide-extension/src/node/sketches-service-impl.ts @@ -561,14 +561,18 @@ void loop() { return path.join(os.tmpdir(), `arduino-ide2-${suffix}`); } - notifyDeleteSketch(sketch: Sketch): void { - const sketchPath = FileUri.fsPath(sketch.uri); - fs.rm(sketchPath, { recursive: true, maxRetries: 5 }, (error) => { - if (error) { - console.error(`Failed to delete sketch at ${sketchPath}.`, error); - } else { - console.error(`Successfully delete sketch at ${sketchPath}.`); - } + async deleteSketch(sketch: Sketch): Promise { + return new Promise((resolve, reject) => { + const sketchPath = FileUri.fsPath(sketch.uri); + fs.rm(sketchPath, { recursive: true, maxRetries: 5 }, (error) => { + if (error) { + console.error(`Failed to delete sketch at ${sketchPath}.`, error); + reject(error); + } else { + console.log(`Successfully deleted sketch at ${sketchPath}.`); + resolve(); + } + }); }); } } diff --git a/arduino-ide-extension/src/node/theia/filesystem/nsfw-watcher/index.ts b/arduino-ide-extension/src/node/theia/filesystem/nsfw-watcher/index.ts new file mode 100644 index 000000000..93e0d267a --- /dev/null +++ b/arduino-ide-extension/src/node/theia/filesystem/nsfw-watcher/index.ts @@ -0,0 +1,31 @@ +import * as yargs from '@theia/core/shared/yargs'; +import { JsonRpcProxyFactory } from '@theia/core'; +import { NoDelayDisposalTimeoutNsfwFileSystemWatcherService } from './nsfw-filesystem-service'; +import type { IPCEntryPoint } from '@theia/core/lib/node/messaging/ipc-protocol'; +import type { FileSystemWatcherServiceClient } from '@theia/filesystem/lib/common/filesystem-watcher-protocol'; + +const options: { + verbose: boolean; +} = yargs + .option('verbose', { + default: false, + alias: 'v', + type: 'boolean', + }) + .option('nsfwOptions', { + alias: 'o', + type: 'string', + coerce: JSON.parse, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }).argv as any; + +export default ((connection) => { + const server = new NoDelayDisposalTimeoutNsfwFileSystemWatcherService( + options + ); + const factory = new JsonRpcProxyFactory( + server + ); + server.setClient(factory.createProxy()); + factory.listen(connection); +}); diff --git a/arduino-ide-extension/src/node/theia/filesystem/nsfw-watcher/nsfw-bindings.ts b/arduino-ide-extension/src/node/theia/filesystem/nsfw-watcher/nsfw-bindings.ts new file mode 100644 index 000000000..77cdd11d7 --- /dev/null +++ b/arduino-ide-extension/src/node/theia/filesystem/nsfw-watcher/nsfw-bindings.ts @@ -0,0 +1,42 @@ +import { join } from 'path'; +import { interfaces } from '@theia/core/shared/inversify'; +import { + NsfwFileSystemWatcherServiceProcessOptions, + NSFW_SINGLE_THREADED, + spawnNsfwFileSystemWatcherServiceProcess, +} from '@theia/filesystem/lib/node/filesystem-backend-module'; +import { FileSystemWatcherService } from '@theia/filesystem/lib/common/filesystem-watcher-protocol'; +import { NsfwFileSystemWatcherServerOptions } from '@theia/filesystem/lib/node/nsfw-watcher/nsfw-filesystem-service'; +import { FileSystemWatcherServiceDispatcher } from '@theia/filesystem/lib/node/filesystem-watcher-dispatcher'; +import { NoDelayDisposalTimeoutNsfwFileSystemWatcherService } from './nsfw-filesystem-service'; + +export function rebindNsfwFileSystemWatcher(rebind: interfaces.Rebind): void { + rebind( + NsfwFileSystemWatcherServiceProcessOptions + ).toConstantValue({ + entryPoint: join(__dirname, 'index.js'), + }); + rebind(FileSystemWatcherService) + .toDynamicValue((context) => + NSFW_SINGLE_THREADED + ? createNsfwFileSystemWatcherService(context) + : spawnNsfwFileSystemWatcherServiceProcess(context) + ) + .inSingletonScope(); +} + +function createNsfwFileSystemWatcherService({ + container, +}: interfaces.Context): FileSystemWatcherService { + const options = container.get( + NsfwFileSystemWatcherServerOptions + ); + const dispatcher = container.get( + FileSystemWatcherServiceDispatcher + ); + const server = new NoDelayDisposalTimeoutNsfwFileSystemWatcherService( + options + ); + server.setClient(dispatcher); + return server; +} diff --git a/arduino-ide-extension/src/node/theia/filesystem/nsfw-watcher/nsfw-filesystem-service.ts b/arduino-ide-extension/src/node/theia/filesystem/nsfw-watcher/nsfw-filesystem-service.ts new file mode 100644 index 000000000..6bf40ead2 --- /dev/null +++ b/arduino-ide-extension/src/node/theia/filesystem/nsfw-watcher/nsfw-filesystem-service.ts @@ -0,0 +1,32 @@ +import { Minimatch } from 'minimatch'; +import type { WatchOptions } from '@theia/filesystem/lib/common/filesystem-watcher-protocol'; +import { + NsfwFileSystemWatcherService, + NsfwWatcher, +} from '@theia/filesystem/lib/node/nsfw-watcher/nsfw-filesystem-service'; + +// Dispose the watcher immediately when the last reference is removed. By default, Theia waits 10 sec. +// https://github.com/eclipse-theia/theia/issues/11639#issuecomment-1238980708 +const NoDelay = 0; + +export class NoDelayDisposalTimeoutNsfwFileSystemWatcherService extends NsfwFileSystemWatcherService { + protected override createWatcher( + clientId: number, + fsPath: string, + options: WatchOptions + ): NsfwWatcher { + const watcherOptions = { + ignored: options.ignored.map( + (pattern) => new Minimatch(pattern, { dot: true }) + ), + }; + return new NsfwWatcher( + clientId, + fsPath, + watcherOptions, + this.options, + this.maybeClient, + NoDelay + ); + } +} diff --git a/browser-app/package.json b/browser-app/package.json deleted file mode 100644 index c7a9dacd3..000000000 --- a/browser-app/package.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "private": true, - "name": "browser-app", - "version": "2.0.0", - "license": "AGPL-3.0-or-later", - "dependencies": { - "@theia/core": "1.25.0", - "@theia/debug": "1.25.0", - "@theia/editor": "1.25.0", - "@theia/file-search": "1.25.0", - "@theia/filesystem": "1.25.0", - "@theia/keymaps": "1.25.0", - "@theia/messages": "1.25.0", - "@theia/monaco": "1.25.0", - "@theia/navigator": "1.25.0", - "@theia/plugin-ext": "1.25.0", - "@theia/plugin-ext-vscode": "1.25.0", - "@theia/preferences": "1.25.0", - "@theia/process": "1.25.0", - "@theia/terminal": "1.25.0", - "@theia/workspace": "1.25.0", - "arduino-ide-extension": "2.0.0" - }, - "devDependencies": { - "@theia/cli": "1.25.0" - }, - "scripts": { - "prepare": "theia build --mode development", - "start": "theia start --plugins=local-dir:../plugins", - "watch": "theia build --watch --mode development" - }, - "theia": { - "frontend": { - "config": { - "applicationName": "Arduino IDE", - "defaultTheme": "arduino-theme", - "preferences": { - "files.autoSave": "afterDelay", - "editor.minimap.enabled": false, - "editor.tabSize": 2, - "editor.scrollBeyondLastLine": false, - "editor.quickSuggestions": { - "other": false, - "comments": false, - "strings": false - }, - "breadcrumbs.enabled": false - } - } - }, - "backend": { - "config": { - "configDirName": ".arduinoIDE" - } - }, - "generator": { - "config": { - "preloadTemplate": "
" - } - } - } -} diff --git a/browser-app/webpack.config.js b/browser-app/webpack.config.js deleted file mode 100644 index 38baa16f8..000000000 --- a/browser-app/webpack.config.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * This file can be edited to customize webpack configuration. - * To reset delete this file and rerun theia build again. - */ -// @ts-check -const config = require('./gen-webpack.config.js'); - -config.resolve.fallback['http'] = false; -config.resolve.fallback['fs'] = false; - -/** - * Expose bundled modules on window.theia.moduleName namespace, e.g. - * window['theia']['@theia/core/lib/common/uri']. - * Such syntax can be used by external code, for instance, for testing. -config.module.rules.push({ - test: /\.js$/, - loader: require.resolve('@theia/application-manager/lib/expose-loader') -}); */ - -module.exports = config; \ No newline at end of file diff --git a/docs/internal/release-procedure.md b/docs/internal/release-procedure.md index d0c034f7c..b95916367 100644 --- a/docs/internal/release-procedure.md +++ b/docs/internal/release-procedure.md @@ -12,7 +12,7 @@ https://github.com/arduino/arduino-ide/pulls/app%2Fgithub-actions ## ⚙ Create the release on GitHub -First of all, you need to **set the new version in all the `package.json` files** across the app (`./package.json`, `./arduino-ide-extension/package.json`, `./browser-app/package.json`, `./electron-app/package.json`), create a PR, and merge it on the `main` branch. +First of all, you need to **set the new version in all the `package.json` files** across the app (`./package.json`, `./arduino-ide-extension/package.json`, and `./electron-app/package.json`), create a PR, and merge it on the `main` branch. To do so, you can make use of the `update:version` script. diff --git a/electron/packager/index.js b/electron/packager/index.js index eb9430cab..12b36097a 100644 --- a/electron/packager/index.js +++ b/electron/packager/index.js @@ -119,14 +119,14 @@ } verifyVersions(allDependencies); - //-------------------------------------------------------------+ - // Save some time: no need to build the `browser-app` example. | - //-------------------------------------------------------------+ + //---------------------------------------------------------------------------------------------------+ + // Save some time: no need to build the projects that are not needed in final app. Currently unused. | + //---------------------------------------------------------------------------------------------------+ //@ts-ignore let pkg = require('../working-copy/package.json'); const workspaces = pkg.workspaces; // We cannot remove the `electron-app`. Otherwise, there is not way to collect the unused dependencies. - const dependenciesToRemove = ['browser-app']; + const dependenciesToRemove = []; for (const dependencyToRemove of dependenciesToRemove) { const index = workspaces.indexOf(dependencyToRemove); if (index !== -1) { diff --git a/package.json b/package.json index 0e0b28e37..9800d23e7 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ }, "scripts": { "prepare": "lerna run prepare && yarn download:plugins", - "cleanup": "npx rimraf ./**/node_modules && rm -rf ./node_modules ./.browser_modules ./arduino-ide-extension/build ./arduino-ide-extension/downloads ./arduino-ide-extension/Examples ./arduino-ide-extension/lib ./browser-app/lib ./browser-app/src-gen ./browser-app/gen-webpack.config.js ./electron-app/lib ./electron-app/src-gen ./electron-app/gen-webpack.config.js", + "cleanup": "npx rimraf ./**/node_modules && rm -rf ./node_modules ./.browser_modules ./arduino-ide-extension/build ./arduino-ide-extension/downloads ./arduino-ide-extension/Examples ./arduino-ide-extension/lib ./electron-app/lib ./electron-app/src-gen ./electron-app/gen-webpack.config.js", "rebuild:browser": "theia rebuild:browser", "rebuild:electron": "theia rebuild:electron", "start": "yarn --cwd ./electron-app start", @@ -49,7 +49,7 @@ "test": "lerna run test", "download:plugins": "theia download:plugins", "update:version": "node ./scripts/update-version.js", - "i18n:generate": "theia nls-extract -e vscode -f \"+(arduino-ide-extension|browser-app|electron-app|plugins)/**/*.ts?(x)\" -o ./i18n/en.json", + "i18n:generate": "theia nls-extract -e vscode -f \"+(arduino-ide-extension|electron-app|plugins)/**/*.ts?(x)\" -o ./i18n/en.json", "i18n:check": "yarn i18n:generate && git add -N ./i18n && git diff --exit-code ./i18n", "i18n:push": "node ./scripts/i18n/transifex-push.js ./i18n/en.json", "i18n:pull": "node ./scripts/i18n/transifex-pull.js ./i18n/", @@ -69,8 +69,7 @@ }, "workspaces": [ "arduino-ide-extension", - "electron-app", - "browser-app" + "electron-app" ], "theiaPluginsDir": "plugins", "theiaPlugins": { diff --git a/scripts/update-version.js b/scripts/update-version.js index f504e5258..fea65ed27 100644 --- a/scripts/update-version.js +++ b/scripts/update-version.js @@ -27,7 +27,6 @@ console.log(`🛠️ Updating current version from '${currentVersion}' to '${tar for (const toUpdate of [ path.join(repoRootPath, 'package.json'), path.join(repoRootPath, 'electron-app', 'package.json'), - path.join(repoRootPath, 'browser-app', 'package.json'), path.join(repoRootPath, 'arduino-ide-extension', 'package.json') ]) { process.stdout.write(` Updating ${toUpdate}'...`);