From f4131f358901c4c08fcb3137c8facc73fd759a0a Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Sun, 18 Jun 2023 23:49:38 +0200 Subject: [PATCH 01/12] Implement remote SSH feature --- .github/workflows/native-dependencies.yml | 49 +++ .gitignore | 1 + examples/browser/package.json | 1 + examples/browser/tsconfig.json | 3 + examples/electron/package.json | 1 + examples/electron/tsconfig.json | 3 + package.json | 4 +- packages/core/package.json | 5 + .../browser/common-frontend-contribution.ts | 18 + .../browser/frontend-application-module.ts | 10 +- packages/core/src/browser/index.ts | 1 + .../messaging/messaging-frontend-module.ts | 3 +- .../messaging/ws-connection-provider.ts | 61 ++- packages/core/src/browser/remote-service.ts | 30 ++ .../src/browser/status-bar/status-bar.tsx | 8 +- .../core/src/browser/style/status-bar.css | 16 +- .../core/src/browser/window/window-service.ts | 6 +- packages/core/src/common/index.ts | 1 + .../core/src/common/quick-pick-service.ts | 1 + packages/core/src/common/version.ts | 17 + packages/core/src/common/window.ts | 4 + .../keyboard/electron-keyboard-module.ts | 2 +- .../electron-local-ws-connection-provider.ts | 45 +++ .../electron-messaging-frontend-module.ts | 15 +- .../window/electron-window-service.ts | 15 +- .../electron-main-window-service.ts | 4 +- .../electron-main-application.ts | 14 +- .../electron-main-window-service-impl.ts | 6 +- .../hosting/electron-ws-origin-validator.ts | 10 +- .../token/electron-token-validator.ts | 14 +- .../src/node/backend-application-module.ts | 3 +- packages/core/src/node/backend-application.ts | 4 + packages/core/src/node/keytar-server.ts | 78 +++- .../node/messaging/messaging-contribution.ts | 13 +- .../app-native-dependency-contribution.ts | 38 ++ .../src/node/remote/backend-remote-module.ts | 35 ++ .../src/node/remote/backend-remote-service.ts | 25 ++ .../src/node/remote/core-copy-contribution.ts | 28 ++ packages/core/src/node/remote/index.ts | 20 + .../node/remote/remote-copy-contribution.ts | 85 +++++ .../remote-native-dependency-contribution.ts | 71 ++++ packages/filesystem/package.json | 4 +- .../electron-file-dialog-module.ts | 4 +- .../electron-file-dialog-service.ts | 8 + .../src/browser/monaco-frontend-module.ts | 9 +- .../src/main/node/plugin-service.ts | 12 +- packages/remote/.eslintrc.js | 10 + packages/remote/README.md | 60 +++ packages/remote/package.json | 64 ++++ .../remote-frontend-contribution.ts | 145 +++++++ .../remote-frontend-module.ts | 43 +++ .../remote-registry-contribution.ts | 70 ++++ .../electron-browser/remote-service-impl.ts | 28 ++ .../remote-ssh-contribution.ts | 86 +++++ .../remote-ssh-connection-provider.ts | 29 ++ .../electron-common/remote-status-service.ts | 35 ++ .../backend-remote-service-impl.ts | 45 +++ .../electron-node/remote-backend-module.ts | 68 ++++ .../remote-connection-service.ts | 60 +++ .../remote-connection-socket-provider.ts | 34 ++ .../remote-proxy-server-provider.ts | 37 ++ .../electron-node/remote-status-service.ts | 41 ++ .../electron-node/remote-tunnel-service.ts | 56 +++ .../remote/src/electron-node/remote-types.ts | 83 ++++ .../setup/remote-copy-service.ts | 114 ++++++ .../setup/remote-native-dependency-service.ts | 110 ++++++ .../setup/remote-node-setup-service.ts | 89 +++++ .../setup/remote-setup-script-service.ts | 57 +++ .../setup/remote-setup-service.ts | 163 ++++++++ .../ssh/remote-ssh-connection-provider.ts | 355 ++++++++++++++++++ .../ssh/ssh-identity-file-collector.ts | 137 +++++++ packages/remote/src/package.spec.ts | 29 ++ packages/remote/tsconfig.json | 16 + scripts/zip-native-dependencies.js | 53 +++ tsconfig.json | 3 + yarn.lock | 205 +++++++++- 76 files changed, 3049 insertions(+), 81 deletions(-) create mode 100644 .github/workflows/native-dependencies.yml create mode 100644 packages/core/src/browser/remote-service.ts create mode 100644 packages/core/src/common/version.ts create mode 100644 packages/core/src/electron-browser/messaging/electron-local-ws-connection-provider.ts create mode 100644 packages/core/src/node/remote/app-native-dependency-contribution.ts create mode 100644 packages/core/src/node/remote/backend-remote-module.ts create mode 100644 packages/core/src/node/remote/backend-remote-service.ts create mode 100644 packages/core/src/node/remote/core-copy-contribution.ts create mode 100644 packages/core/src/node/remote/index.ts create mode 100644 packages/core/src/node/remote/remote-copy-contribution.ts create mode 100644 packages/core/src/node/remote/remote-native-dependency-contribution.ts create mode 100644 packages/remote/.eslintrc.js create mode 100644 packages/remote/README.md create mode 100644 packages/remote/package.json create mode 100644 packages/remote/src/electron-browser/remote-frontend-contribution.ts create mode 100644 packages/remote/src/electron-browser/remote-frontend-module.ts create mode 100644 packages/remote/src/electron-browser/remote-registry-contribution.ts create mode 100644 packages/remote/src/electron-browser/remote-service-impl.ts create mode 100644 packages/remote/src/electron-browser/remote-ssh-contribution.ts create mode 100644 packages/remote/src/electron-common/remote-ssh-connection-provider.ts create mode 100644 packages/remote/src/electron-common/remote-status-service.ts create mode 100644 packages/remote/src/electron-node/backend-remote-service-impl.ts create mode 100644 packages/remote/src/electron-node/remote-backend-module.ts create mode 100644 packages/remote/src/electron-node/remote-connection-service.ts create mode 100644 packages/remote/src/electron-node/remote-connection-socket-provider.ts create mode 100644 packages/remote/src/electron-node/remote-proxy-server-provider.ts create mode 100644 packages/remote/src/electron-node/remote-status-service.ts create mode 100644 packages/remote/src/electron-node/remote-tunnel-service.ts create mode 100644 packages/remote/src/electron-node/remote-types.ts create mode 100644 packages/remote/src/electron-node/setup/remote-copy-service.ts create mode 100644 packages/remote/src/electron-node/setup/remote-native-dependency-service.ts create mode 100644 packages/remote/src/electron-node/setup/remote-node-setup-service.ts create mode 100644 packages/remote/src/electron-node/setup/remote-setup-script-service.ts create mode 100644 packages/remote/src/electron-node/setup/remote-setup-service.ts create mode 100644 packages/remote/src/electron-node/ssh/remote-ssh-connection-provider.ts create mode 100644 packages/remote/src/electron-node/ssh/ssh-identity-file-collector.ts create mode 100644 packages/remote/src/package.spec.ts create mode 100644 packages/remote/tsconfig.json create mode 100644 scripts/zip-native-dependencies.js diff --git a/.github/workflows/native-dependencies.yml b/.github/workflows/native-dependencies.yml new file mode 100644 index 0000000000000..10096bc61314c --- /dev/null +++ b/.github/workflows/native-dependencies.yml @@ -0,0 +1,49 @@ +name: Package Native Dependencies + +on: workflow_dispatch + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: ['ubuntu-20.04', 'windows-latest', 'macos-latest'] + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Use Node.js 18.17.0 + uses: actions/setup-node@v3 + with: + node-version: '18.17.0' + registry-url: 'https://registry.npmjs.org' + + - name: Use Python 3.x + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Install and Build + shell: bash + run: | + yarn --skip-integrity-check --network-timeout 100000 + env: + NODE_OPTIONS: --max_old_space_size=4096 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # https://github.com/microsoft/vscode-ripgrep/issues/9 + + - name: Build Browser App + shell: bash + run: | + yarn browser build + env: + NODE_OPTIONS: --max_old_space_size=4096 + + - name: Zip Native Dependencies + shell: bash + run: yarn zip:native:dependencies + + - name: Upload Artifacts + uses: actions/upload-artifact@v3 + with: + name: native-dependencies + path: ./scripts/native-dependencies-*.zip diff --git a/.gitignore b/.gitignore index 5bdb44c92a06d..d8cde8f9d7d9a 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ dependency-check-summary.txt* .tours /performance-result.json *.vsix +/scripts/native-dependencies-* diff --git a/examples/browser/package.json b/examples/browser/package.json index c319009eb7585..dde99f6c11935 100644 --- a/examples/browser/package.json +++ b/examples/browser/package.json @@ -45,6 +45,7 @@ "@theia/preview": "1.42.0", "@theia/process": "1.42.0", "@theia/property-view": "1.42.0", + "@theia/remote": "1.42.0", "@theia/scm": "1.42.0", "@theia/scm-extra": "1.42.0", "@theia/search-in-workspace": "1.42.0", diff --git a/examples/browser/tsconfig.json b/examples/browser/tsconfig.json index 48955ff9e6ca9..e3756e4f8c7de 100644 --- a/examples/browser/tsconfig.json +++ b/examples/browser/tsconfig.json @@ -98,6 +98,9 @@ { "path": "../../packages/property-view" }, + { + "path": "../../packages/remote" + }, { "path": "../../packages/scm" }, diff --git a/examples/electron/package.json b/examples/electron/package.json index ef1fd825b7ae0..ada2a8106426d 100644 --- a/examples/electron/package.json +++ b/examples/electron/package.json @@ -45,6 +45,7 @@ "@theia/preview": "1.42.0", "@theia/process": "1.42.0", "@theia/property-view": "1.42.0", + "@theia/remote": "1.42.0", "@theia/scm": "1.42.0", "@theia/scm-extra": "1.42.0", "@theia/search-in-workspace": "1.42.0", diff --git a/examples/electron/tsconfig.json b/examples/electron/tsconfig.json index ccd4b9fbd4d34..edac6071a7d07 100644 --- a/examples/electron/tsconfig.json +++ b/examples/electron/tsconfig.json @@ -98,6 +98,9 @@ { "path": "../../packages/property-view" }, + { + "path": "../../packages/remote" + }, { "path": "../../packages/scm" }, diff --git a/package.json b/package.json index a3ea4050b234a..eedb0306c48a5 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@typescript-eslint/eslint-plugin-tslint": "^4.8.1", "@typescript-eslint/parser": "^4.8.1", "@vscode/vsce": "^2.15.0", + "archiver": "^5.3.1", "chai": "4.3.10", "chai-spies": "1.0.0", "chai-string": "^1.4.0", @@ -90,7 +91,8 @@ "watch:compile": "concurrently --kill-others -n cleanup,tsc -c magenta,red \"ts-clean dev-packages/* packages/* -w\" \"tsc -b -w --preserveWatchOutput\"", "performance:startup": "yarn -s performance:startup:browser && yarn -s performance:startup:electron", "performance:startup:browser": "concurrently --success first -k -r \"cd scripts/performance && node browser-performance.js --name 'Browser Frontend Startup' --folder browser --runs 10\" \"yarn -s --cwd examples/browser start\"", - "performance:startup:electron": "yarn -s electron rebuild && cd scripts/performance && node electron-performance.js --name 'Electron Frontend Startup' --folder electron --runs 10" + "performance:startup:electron": "yarn -s electron rebuild && cd scripts/performance && node electron-performance.js --name 'Electron Frontend Startup' --folder electron --runs 10", + "zip:native:dependencies": "node ./scripts/zip-native-dependencies.js" }, "workspaces": [ "dev-packages/*", diff --git a/packages/core/package.json b/packages/core/package.json index b11b4c87047f3..513e0bce42bf1 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -23,6 +23,7 @@ "@types/dompurify": "^2.2.2", "@types/express": "^4.16.0", "@types/fs-extra": "^4.0.2", + "@types/glob": "^8.1.0", "@types/lodash.debounce": "4.0.3", "@types/lodash.throttle": "^4.1.3", "@types/markdown-it": "^12.2.3", @@ -46,6 +47,7 @@ "font-awesome": "^4.7.0", "fs-extra": "^4.0.2", "fuzzy": "^0.1.3", + "glob": "^8.1.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "iconv-lite": "^0.6.0", @@ -166,6 +168,9 @@ { "backend": "lib/node/request/backend-request-module", "backendElectron": "lib/electron-node/request/electron-backend-request-module" + }, + { + "backend": "lib/node/remote/backend-remote-module" } ], "keywords": [ diff --git a/packages/core/src/browser/common-frontend-contribution.ts b/packages/core/src/browser/common-frontend-contribution.ts index f4a7db93778d4..8a5e53f3a044b 100644 --- a/packages/core/src/browser/common-frontend-contribution.ts +++ b/packages/core/src/browser/common-frontend-contribution.ts @@ -2300,6 +2300,24 @@ export class CommonFrontendContribution implements FrontendApplicationContributi hcLight: Color.lighten('statusBar.offlineBackground', 0.6) }, description: 'Background of active statusbar item in case the theia server is offline.' }, + { + id: 'statusBarItem.remoteBackground', + defaults: { + dark: 'activityBarBadge.background', + light: 'activityBarBadge.background', + hcDark: 'activityBarBadge.background', + hcLight: 'activityBarBadge.background' + }, description: 'Background color for the remote indicator on the status bar.' + }, + { + id: 'statusBarItem.remoteForeground', + defaults: { + dark: 'activityBarBadge.foreground', + light: 'activityBarBadge.foreground', + hcDark: 'activityBarBadge.foreground', + hcLight: 'activityBarBadge.foreground' + }, description: 'Foreground color for the remote indicator on the status bar.' + }, // Buttons { id: 'secondaryButton.foreground', diff --git a/packages/core/src/browser/frontend-application-module.ts b/packages/core/src/browser/frontend-application-module.ts index 9b2531485382a..13b7b254f6917 100644 --- a/packages/core/src/browser/frontend-application-module.ts +++ b/packages/core/src/browser/frontend-application-module.ts @@ -137,6 +137,7 @@ import { MarkdownRenderer, MarkdownRendererFactory, MarkdownRendererImpl } from import { StylingParticipant, StylingService } from './styling-service'; import { bindCommonStylingParticipants } from './common-styling-participants'; import { HoverService } from './hover-service'; +import { NullRemoteService, RemoteService } from './remote-service'; import { AdditionalViewsMenuWidget, AdditionalViewsMenuWidgetFactory } from './shell/additional-views-menu-widget'; export { bindResourceProvider, bindMessageService, bindPreferenceService }; @@ -248,7 +249,7 @@ export const frontendApplicationModule = new ContainerModule((bind, _unbind, _is bind(SelectionService).toSelf().inSingletonScope(); bind(CommandRegistry).toSelf().inSingletonScope().onActivation(({ container }, registry) => { - WebSocketConnectionProvider.createProxy(container, commandServicePath, registry); + WebSocketConnectionProvider.createDualProxy(container, commandServicePath, registry); return registry; }); bind(CommandService).toService(CommandRegistry); @@ -268,7 +269,7 @@ export const frontendApplicationModule = new ContainerModule((bind, _unbind, _is bindMessageService(bind).onActivation(({ container }, messages) => { const client = container.get(MessageClient); - WebSocketConnectionProvider.createProxy(container, messageServicePath, client); + WebSocketConnectionProvider.createDualProxy(container, messageServicePath, client); return messages; }); @@ -296,7 +297,7 @@ export const frontendApplicationModule = new ContainerModule((bind, _unbind, _is bind(QuickAccessContribution).toService(QuickHelpService); bind(QuickPickService).to(QuickPickServiceImpl).inSingletonScope().onActivation(({ container }, quickPickService: QuickPickService) => { - WebSocketConnectionProvider.createProxy(container, quickPickServicePath, quickPickService); + WebSocketConnectionProvider.createDualProxy(container, quickPickServicePath, quickPickService); return quickPickService; }); @@ -449,5 +450,8 @@ export const frontendApplicationModule = new ContainerModule((bind, _unbind, _is bindContributionProvider(bind, StylingParticipant); bind(FrontendApplicationContribution).toService(StylingService); + bind(NullRemoteService).toSelf().inSingletonScope(); + bind(RemoteService).toService(NullRemoteService); + bind(SecondaryWindowHandler).toSelf().inSingletonScope(); }); diff --git a/packages/core/src/browser/index.ts b/packages/core/src/browser/index.ts index eecaa565401dc..889859bfa8dbd 100644 --- a/packages/core/src/browser/index.ts +++ b/packages/core/src/browser/index.ts @@ -45,3 +45,4 @@ export * from './tooltip-service'; export * from './decoration-style'; export * from './styling-service'; export * from './hover-service'; +export * from './remote-service'; diff --git a/packages/core/src/browser/messaging/messaging-frontend-module.ts b/packages/core/src/browser/messaging/messaging-frontend-module.ts index 97329ceccbed7..f852c8f651231 100644 --- a/packages/core/src/browser/messaging/messaging-frontend-module.ts +++ b/packages/core/src/browser/messaging/messaging-frontend-module.ts @@ -15,8 +15,9 @@ // ***************************************************************************** import { ContainerModule } from 'inversify'; -import { WebSocketConnectionProvider } from './ws-connection-provider'; +import { LocalWebSocketConnectionProvider, WebSocketConnectionProvider } from './ws-connection-provider'; export const messagingFrontendModule = new ContainerModule(bind => { bind(WebSocketConnectionProvider).toSelf().inSingletonScope(); + bind(LocalWebSocketConnectionProvider).toService(WebSocketConnectionProvider); }); diff --git a/packages/core/src/browser/messaging/ws-connection-provider.ts b/packages/core/src/browser/messaging/ws-connection-provider.ts index a5ad8ab4437c4..d91bf2d518568 100644 --- a/packages/core/src/browser/messaging/ws-connection-provider.ts +++ b/packages/core/src/browser/messaging/ws-connection-provider.ts @@ -14,7 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { injectable, interfaces, decorate, unmanaged } from 'inversify'; +import { injectable, interfaces, decorate, unmanaged, postConstruct } from 'inversify'; import { RpcProxyFactory, RpcProxy, Emitter, Event, Channel } from '../../common'; import { Endpoint } from '../endpoint'; import { AbstractConnectionProvider } from '../../common/messaging/abstract-connection-provider'; @@ -24,6 +24,8 @@ import { IWebSocket, WebSocketChannel } from '../../common/messaging/web-socket- decorate(injectable(), RpcProxyFactory); decorate(unmanaged(), RpcProxyFactory, 0); +export const LocalWebSocketConnectionProvider = Symbol('LocalWebSocketConnectionProvider'); + export interface WebSocketOptions { /** * True by default. @@ -48,23 +50,28 @@ export class WebSocketConnectionProvider extends AbstractConnectionProvider(path, arg); } - protected readonly socket: Socket; + static createLocalProxy(container: interfaces.Container, path: string, arg?: object): RpcProxy { + return container.get(LocalWebSocketConnectionProvider).createProxy(path, arg); + } + + static createDualProxy(container: interfaces.Container, path: string, arg?: object): void { + const remote = container.get(WebSocketConnectionProvider); + const local = container.get(LocalWebSocketConnectionProvider); + remote.createProxy(path, arg); + if (remote !== local) { + local.createProxy(path, arg); + } + } + + protected socket: Socket; constructor() { super(); - const url = this.createWebSocketUrl(WebSocketChannel.wsPath); - this.socket = this.createWebSocket(url); - this.socket.on('connect', () => { - this.initializeMultiplexer(); - if (this.reconnectChannelOpeners.length > 0) { - this.reconnectChannelOpeners.forEach(opener => opener()); - this.reconnectChannelOpeners = []; - } - this.socket.on('disconnect', () => this.fireSocketDidClose()); - this.socket.on('message', () => this.onIncomingMessageActivityEmitter.fire(undefined)); - this.fireSocketDidOpen(); - }); - this.socket.connect(); + } + + @postConstruct() + protected init(): void { + this.connect(); } protected createMainChannel(): Channel { @@ -104,11 +111,31 @@ export class WebSocketConnectionProvider extends AbstractConnectionProvider { + this.initializeMultiplexer(); + if (this.reconnectChannelOpeners.length > 0) { + this.reconnectChannelOpeners.forEach(opener => opener()); + this.reconnectChannelOpeners = []; + } + this.socket.on('disconnect', () => this.fireSocketDidClose()); + this.socket.on('message', () => this.onIncomingMessageActivityEmitter.fire(undefined)); + this.fireSocketDidOpen(); + }); + this.socket.connect(); } /** diff --git a/packages/core/src/browser/remote-service.ts b/packages/core/src/browser/remote-service.ts new file mode 100644 index 0000000000000..e515c66b692c8 --- /dev/null +++ b/packages/core/src/browser/remote-service.ts @@ -0,0 +1,30 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { injectable } from 'inversify'; + +export const RemoteService = Symbol('RemoteService'); + +export interface RemoteService { + isConnected(): boolean; +} + +@injectable() +export class NullRemoteService implements RemoteService { + isConnected(): boolean { + return false; + } +} diff --git a/packages/core/src/browser/status-bar/status-bar.tsx b/packages/core/src/browser/status-bar/status-bar.tsx index 7325ad86c22f7..025f8dece41b2 100644 --- a/packages/core/src/browser/status-bar/status-bar.tsx +++ b/packages/core/src/browser/status-bar/status-bar.tsx @@ -151,6 +151,9 @@ export class StatusBarImpl extends ReactWidget implements StatusBar { } else { attrs['aria-label'] = [entry.text, entry.tooltip].join(', '); } + if (entry.backgroundColor) { + attrs.className += ' has-background'; + } attrs.style = { color: entry.color || this.color, @@ -178,8 +181,9 @@ export class StatusBarImpl extends ReactWidget implements StatusBar { children.push({val}); } }); - const elementInnerDiv = {children}; - return React.createElement('div', { key: entry.id, ...this.createAttributes(entry) }, elementInnerDiv); + return
+ {children} +
; } } diff --git a/packages/core/src/browser/style/status-bar.css b/packages/core/src/browser/style/status-bar.css index 84c53eda5af0c..a562a4b475c8d 100644 --- a/packages/core/src/browser/style/status-bar.css +++ b/packages/core/src/browser/style/status-bar.css @@ -30,7 +30,7 @@ body.theia-no-open-workspace #theia-statusBar { background: var(--theia-statusBar-noFolderBackground); color: var(--theia-statusBar-noFolderForeground); border-top: var(--theia-border-width) solid - var(--theia-statusBar-noFolderBorder); + var(--theia-statusBar-noFolderBorder); } #theia-statusBar .area { @@ -41,7 +41,6 @@ body.theia-no-open-workspace #theia-statusBar { #theia-statusBar .area.left { justify-content: flex-start; - padding-left: calc(var(--theia-ui-padding) * 2); } #theia-statusBar .area.right { @@ -56,8 +55,14 @@ body.theia-no-open-workspace #theia-statusBar { font-size: var(--theia-statusBar-font-size); } -#theia-statusBar .area .element > * { - margin-left: calc(var(--theia-ui-padding) / 2); +#theia-statusBar .area.left .element.has-background { + margin-left: 0px; + margin-right: 3px; + padding-left: 7px; + padding-right: 7px; +} +#theia-statusBar .area .element>* { + margin-left: calc(var(--theia-ui-padding)/2); } #theia-statusBar .area .element .codicon { @@ -77,6 +82,9 @@ body.theia-no-open-workspace #theia-statusBar { color: var(--theia-statusBar-offlineForeground) !important; } +#theia-statusBar .area.left .element:first-child:not(.has-background) { + margin-left: calc(var(--theia-ui-padding) * 3); +} #theia-statusBar .area.left .element { margin-right: var(--theia-ui-padding); } diff --git a/packages/core/src/browser/window/window-service.ts b/packages/core/src/browser/window/window-service.ts index de04e6e7e333f..34adb43737c22 100644 --- a/packages/core/src/browser/window/window-service.ts +++ b/packages/core/src/browser/window/window-service.ts @@ -16,7 +16,7 @@ import { StopReason } from '../../common/frontend-application-state'; import { Event } from '../../common/event'; -import { NewWindowOptions } from '../../common/window'; +import { NewWindowOptions, WindowSearchParams } from '../../common/window'; /** * Service for opening new browser windows. @@ -35,7 +35,7 @@ export interface WindowService { * Opens a new default window. * - In electron and in the browser it will open the default window without a pre-defined content. */ - openNewDefaultWindow(): void; + openNewDefaultWindow(params?: WindowSearchParams): void; /** * Fires when the `window` unloads. The unload event is inevitable. On this event, the frontend application can save its state and release resource. @@ -64,5 +64,5 @@ export interface WindowService { /** * Reloads the window according to platform. */ - reload(): void; + reload(params?: WindowSearchParams): void; } diff --git a/packages/core/src/common/index.ts b/packages/core/src/common/index.ts index 65d2741d21216..a3f5aeb125e57 100644 --- a/packages/core/src/common/index.ts +++ b/packages/core/src/common/index.ts @@ -47,3 +47,4 @@ export * from './telemetry'; export * from './types'; export { default as URI } from './uri'; export * from './view-column'; +export * from './version'; diff --git a/packages/core/src/common/quick-pick-service.ts b/packages/core/src/common/quick-pick-service.ts index f87b6bfb27797..96a1aba08bd57 100644 --- a/packages/core/src/common/quick-pick-service.ts +++ b/packages/core/src/common/quick-pick-service.ts @@ -272,6 +272,7 @@ export interface QuickPickOptions { onDidTriggerItemButton?: (ItemButtonEvent: QuickPickItemButtonContext) => void } +export const quickInputServicePath = '/services/quickInput'; export const QuickInputService = Symbol('QuickInputService'); export interface QuickInputService { readonly backButton: QuickInputButton; diff --git a/packages/core/src/common/version.ts b/packages/core/src/common/version.ts new file mode 100644 index 0000000000000..fcfc9d11e1f14 --- /dev/null +++ b/packages/core/src/common/version.ts @@ -0,0 +1,17 @@ +// ***************************************************************************** +// Copyright (C) 2017 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +export const THEIA_VERSION: string = require('../../package.json').version; diff --git a/packages/core/src/common/window.ts b/packages/core/src/common/window.ts index f4606c9d43ddc..ca53a1d710b31 100644 --- a/packages/core/src/common/window.ts +++ b/packages/core/src/common/window.ts @@ -28,3 +28,7 @@ export interface NewWindowOptions { */ readonly external?: boolean; } + +export interface WindowSearchParams { + [key: string]: string +} diff --git a/packages/core/src/electron-browser/keyboard/electron-keyboard-module.ts b/packages/core/src/electron-browser/keyboard/electron-keyboard-module.ts index f7168521cbaef..466f734ae46f1 100644 --- a/packages/core/src/electron-browser/keyboard/electron-keyboard-module.ts +++ b/packages/core/src/electron-browser/keyboard/electron-keyboard-module.ts @@ -21,7 +21,7 @@ import { ElectronKeyboardLayoutChangeNotifier } from './electron-keyboard-layout export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(KeyboardLayoutProvider).toDynamicValue(ctx => - WebSocketConnectionProvider.createProxy(ctx.container, keyboardPath) + WebSocketConnectionProvider.createLocalProxy(ctx.container, keyboardPath) ).inSingletonScope(); bind(ElectronKeyboardLayoutChangeNotifier).toSelf().inSingletonScope(); bind(KeyboardLayoutChangeNotifier).toService(ElectronKeyboardLayoutChangeNotifier); diff --git a/packages/core/src/electron-browser/messaging/electron-local-ws-connection-provider.ts b/packages/core/src/electron-browser/messaging/electron-local-ws-connection-provider.ts new file mode 100644 index 0000000000000..01c7912cfe089 --- /dev/null +++ b/packages/core/src/electron-browser/messaging/electron-local-ws-connection-provider.ts @@ -0,0 +1,45 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { injectable } from 'inversify'; +import { WebSocketConnectionProvider } from '../../browser/messaging/ws-connection-provider'; +import { Endpoint } from '../../browser/endpoint'; + +export function getLocalPort(): string | undefined { + const params = new URLSearchParams(location.search); + return params.get('localPort') ?? undefined; +} + +@injectable() +export class ElectronLocalWebSocketConnectionProvider extends WebSocketConnectionProvider { + + protected override createEndpoint(path: string): Endpoint { + const localPort = getLocalPort(); + if (!localPort) { + throw new Error('This should only be called in case there is a local port specified!'); + } + const endpoint = new Endpoint({ + path + }, { + host: `localhost:${localPort}`, + pathname: '/', + protocol: 'http', + search: '' + }); + return endpoint; + } + +} diff --git a/packages/core/src/electron-browser/messaging/electron-messaging-frontend-module.ts b/packages/core/src/electron-browser/messaging/electron-messaging-frontend-module.ts index 253e0d6946127..d62fc02a00607 100644 --- a/packages/core/src/electron-browser/messaging/electron-messaging-frontend-module.ts +++ b/packages/core/src/electron-browser/messaging/electron-messaging-frontend-module.ts @@ -16,13 +16,26 @@ import { ContainerModule } from 'inversify'; import { FrontendApplicationContribution } from '../../browser/frontend-application-contribution'; -import { WebSocketConnectionProvider } from '../../browser/messaging/ws-connection-provider'; +import { LocalWebSocketConnectionProvider, WebSocketConnectionProvider } from '../../browser/messaging/ws-connection-provider'; import { ElectronWebSocketConnectionProvider } from './electron-ws-connection-provider'; import { ElectronIpcConnectionProvider } from './electron-ipc-connection-provider'; +import { ElectronLocalWebSocketConnectionProvider, getLocalPort } from './electron-local-ws-connection-provider'; export const messagingFrontendModule = new ContainerModule(bind => { bind(ElectronWebSocketConnectionProvider).toSelf().inSingletonScope(); bind(FrontendApplicationContribution).toService(ElectronWebSocketConnectionProvider); bind(WebSocketConnectionProvider).toService(ElectronWebSocketConnectionProvider); + bind(ElectronLocalWebSocketConnectionProvider).toSelf().inSingletonScope(); + bind(LocalWebSocketConnectionProvider).toDynamicValue(ctx => { + const localPort = getLocalPort(); + if (localPort) { + // Return new web socket provider that connects to local app + return ctx.container.get(ElectronLocalWebSocketConnectionProvider); + } else { + // Return the usual web socket provider that already established its connection + // That way we don't create a second socket connection + return ctx.container.get(WebSocketConnectionProvider); + } + }).inSingletonScope(); bind(ElectronIpcConnectionProvider).toSelf().inSingletonScope(); }); diff --git a/packages/core/src/electron-browser/window/electron-window-service.ts b/packages/core/src/electron-browser/window/electron-window-service.ts index 9ddab4469887d..44442ed5d4d21 100644 --- a/packages/core/src/electron-browser/window/electron-window-service.ts +++ b/packages/core/src/electron-browser/window/electron-window-service.ts @@ -15,7 +15,7 @@ // ***************************************************************************** import { injectable, inject, postConstruct } from 'inversify'; -import { NewWindowOptions } from '../../common/window'; +import { NewWindowOptions, WindowSearchParams } from '../../common/window'; import { DefaultWindowService } from '../../browser/window/default-window-service'; import { ElectronMainWindowService } from '../../electron-common/electron-main-window-service'; import { ElectronWindowPreferences } from './electron-window-preferences'; @@ -44,8 +44,8 @@ export class ElectronWindowService extends DefaultWindowService { return undefined; } - override openNewDefaultWindow(): void { - this.delegate.openNewDefaultWindow(); + override openNewDefaultWindow(params?: WindowSearchParams): void { + this.delegate.openNewDefaultWindow(params); } @postConstruct() @@ -75,7 +75,12 @@ export class ElectronWindowService extends DefaultWindowService { } } - override reload(): void { - window.electronTheiaCore.requestReload(); + override reload(params?: WindowSearchParams): void { + if (params) { + const query = Object.entries(params).map(([name, value]) => `${name}=${value}`).join('&'); + location.search = query; + } else { + window.electronTheiaCore.requestReload(); + } } } diff --git a/packages/core/src/electron-common/electron-main-window-service.ts b/packages/core/src/electron-common/electron-main-window-service.ts index 9f739631fd84b..95033224a3deb 100644 --- a/packages/core/src/electron-common/electron-main-window-service.ts +++ b/packages/core/src/electron-common/electron-main-window-service.ts @@ -14,11 +14,11 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { NewWindowOptions } from '../common/window'; +import { NewWindowOptions, WindowSearchParams } from '../common/window'; export const electronMainWindowServicePath = '/services/electron-window'; export const ElectronMainWindowService = Symbol('ElectronMainWindowService'); export interface ElectronMainWindowService { openNewWindow(url: string, options?: NewWindowOptions): undefined; - openNewDefaultWindow(): void; + openNewDefaultWindow(params?: WindowSearchParams): void; } diff --git a/packages/core/src/electron-main/electron-main-application.ts b/packages/core/src/electron-main/electron-main-application.ts index 4faaade6461e6..66d6ba1db7ff1 100644 --- a/packages/core/src/electron-main/electron-main-application.ts +++ b/packages/core/src/electron-main/electron-main-application.ts @@ -31,7 +31,7 @@ import { ElectronSecurityTokenService } from './electron-security-token-service' import { ElectronSecurityToken } from '../electron-common/electron-token'; import Storage = require('electron-store'); import { Disposable, DisposableCollection, isOSX, isWindows } from '../common'; -import { DEFAULT_WINDOW_HASH } from '../common/window'; +import { DEFAULT_WINDOW_HASH, WindowSearchParams } from '../common/window'; import { TheiaBrowserWindowOptions, TheiaElectronWindow, TheiaElectronWindowFactory } from './theia-electron-window'; import { ElectronMainApplicationGlobals } from './electron-main-constants'; import { createDisposableListener } from './event-utils'; @@ -343,9 +343,9 @@ export class ElectronMainApplication { }; } - async openDefaultWindow(): Promise { + async openDefaultWindow(params?: WindowSearchParams): Promise { const options = this.getDefaultTheiaWindowOptions(); - const [uri, electronWindow] = await Promise.all([this.createWindowUri(), this.reuseOrCreateWindow(options)]); + const [uri, electronWindow] = await Promise.all([this.createWindowUri(params), this.reuseOrCreateWindow(options)]); electronWindow.loadURL(uri.withFragment(DEFAULT_WINDOW_HASH).toString(true)); return electronWindow; } @@ -419,9 +419,13 @@ export class ElectronMainApplication { } } - protected async createWindowUri(): Promise { + protected async createWindowUri(params: WindowSearchParams = {}): Promise { + if (!('port' in params)) { + params.port = (await this.backendPort).toString(); + } + const query = Object.entries(params).map(([name, value]) => `${name}=${value}`).join('&'); return FileUri.create(this.globals.THEIA_FRONTEND_HTML_PATH) - .withQuery(`port=${await this.backendPort}`); + .withQuery(query); } protected getDefaultTheiaWindowOptions(): TheiaBrowserWindowOptions { diff --git a/packages/core/src/electron-main/electron-main-window-service-impl.ts b/packages/core/src/electron-main/electron-main-window-service-impl.ts index 6a7dcb5c19f37..940258cf31640 100644 --- a/packages/core/src/electron-main/electron-main-window-service-impl.ts +++ b/packages/core/src/electron-main/electron-main-window-service-impl.ts @@ -18,7 +18,7 @@ import { shell } from '@theia/electron/shared/electron'; import { injectable, inject } from 'inversify'; import { ElectronMainWindowService } from '../electron-common/electron-main-window-service'; import { ElectronMainApplication } from './electron-main-application'; -import { NewWindowOptions } from '../common/window'; +import { NewWindowOptions, WindowSearchParams } from '../common/window'; @injectable() export class ElectronMainWindowServiceImpl implements ElectronMainWindowService { @@ -37,8 +37,8 @@ export class ElectronMainWindowServiceImpl implements ElectronMainWindowService return undefined; } - openNewDefaultWindow(): void { - this.app.openDefaultWindow(); + openNewDefaultWindow(params?: WindowSearchParams): void { + this.app.openDefaultWindow(params); } } diff --git a/packages/core/src/electron-node/hosting/electron-ws-origin-validator.ts b/packages/core/src/electron-node/hosting/electron-ws-origin-validator.ts index d725249824085..d9bceab8db793 100644 --- a/packages/core/src/electron-node/hosting/electron-ws-origin-validator.ts +++ b/packages/core/src/electron-node/hosting/electron-ws-origin-validator.ts @@ -15,13 +15,21 @@ // ***************************************************************************** import * as http from 'http'; -import { injectable } from 'inversify'; +import { inject, injectable } from 'inversify'; +import { BackendRemoteService } from '../../node/remote/backend-remote-service'; import { WsRequestValidatorContribution } from '../../node/ws-request-validators'; @injectable() export class ElectronWsOriginValidator implements WsRequestValidatorContribution { + @inject(BackendRemoteService) + protected readonly backendRemoteService: BackendRemoteService; + allowWsUpgrade(request: http.IncomingMessage): boolean { + // If we are running as a remote server, requests will come from an http endpoint + if (this.backendRemoteService.isRemoteServer()) { + return true; + } // On Electron the main page is served from the `file` protocol. // We don't expect the requests to come from anywhere else. return request.headers.origin === 'file://'; diff --git a/packages/core/src/electron-node/token/electron-token-validator.ts b/packages/core/src/electron-node/token/electron-token-validator.ts index 02f3debc19808..cc8da02a8c478 100644 --- a/packages/core/src/electron-node/token/electron-token-validator.ts +++ b/packages/core/src/electron-node/token/electron-token-validator.ts @@ -28,7 +28,7 @@ import { WsRequestValidatorContribution } from '../../node/ws-request-validators @injectable() export class ElectronTokenValidator implements WsRequestValidatorContribution { - protected electronSecurityToken: ElectronSecurityToken; + protected electronSecurityToken?: ElectronSecurityToken; @postConstruct() protected init(): void { @@ -43,6 +43,9 @@ export class ElectronTokenValidator implements WsRequestValidatorContribution { * Expects the token to be passed via cookies by default. */ allowRequest(request: http.IncomingMessage): boolean { + if (!this.electronSecurityToken) { + return true; + } const cookieHeader = request.headers.cookie; if (isString(cookieHeader)) { const token = cookie.parse(cookieHeader)[ElectronSecurityToken]; @@ -76,8 +79,13 @@ export class ElectronTokenValidator implements WsRequestValidatorContribution { /** * Returns the token to compare to when authorizing requests. */ - protected getToken(): ElectronSecurityToken { - return JSON.parse(process.env[ElectronSecurityToken]!); + protected getToken(): ElectronSecurityToken | undefined { + const token = process.env[ElectronSecurityToken]; + if (token) { + return JSON.parse(token); + } else { + return undefined; + } } } diff --git a/packages/core/src/node/backend-application-module.ts b/packages/core/src/node/backend-application-module.ts index 95ed52c1eee87..d414491e83c5b 100644 --- a/packages/core/src/node/backend-application-module.ts +++ b/packages/core/src/node/backend-application-module.ts @@ -29,7 +29,7 @@ import { ApplicationServer, applicationPath } from '../common/application-protoc import { EnvVariablesServer, envVariablesPath } from './../common/env-variables'; import { EnvVariablesServerImpl } from './env-variables'; import { ConnectionContainerModule } from './messaging/connection-container-module'; -import { QuickPickService, quickPickServicePath } from '../common/quick-pick-service'; +import { QuickInputService, quickInputServicePath, QuickPickService, quickPickServicePath } from '../common/quick-pick-service'; import { WsRequestValidator, WsRequestValidatorContribution } from './ws-request-validators'; import { KeytarService, keytarServicePath } from '../common/keytar-protocol'; import { KeytarServiceImpl } from './keytar-server'; @@ -54,6 +54,7 @@ const messageConnectionModule = ConnectionContainerModule.create(({ bind, bindFr }); const quickPickConnectionModule = ConnectionContainerModule.create(({ bindFrontendService }) => { + bindFrontendService(quickInputServicePath, QuickInputService); bindFrontendService(quickPickServicePath, QuickPickService); }); diff --git a/packages/core/src/node/backend-application.ts b/packages/core/src/node/backend-application.ts index 92ec60c1e8c71..b043b3f1ed83d 100644 --- a/packages/core/src/node/backend-application.ts +++ b/packages/core/src/node/backend-application.ts @@ -228,6 +228,10 @@ export class BackendApplication { this.app.get('*.gif', this.serveGzipped.bind(this, 'image/gif')); this.app.get('*.png', this.serveGzipped.bind(this, 'image/png')); this.app.get('*.svg', this.serveGzipped.bind(this, 'image/svg+xml')); + this.app.get('*.eot', this.serveGzipped.bind(this, 'application/vnd.ms-fontobject')); + this.app.get('*.ttf', this.serveGzipped.bind(this, 'font/ttf')); + this.app.get('*.woff', this.serveGzipped.bind(this, 'font/woff')); + this.app.get('*.woff2', this.serveGzipped.bind(this, 'font/woff2')); for (const contribution of this.contributionsProvider.getContributions()) { if (contribution.configure) { diff --git a/packages/core/src/node/keytar-server.ts b/packages/core/src/node/keytar-server.ts index bcec832768957..e1bbf034981f8 100644 --- a/packages/core/src/node/keytar-server.ts +++ b/packages/core/src/node/keytar-server.ts @@ -21,16 +21,23 @@ // code copied and modified from https://github.com/microsoft/vscode/blob/1.55.2/src/vs/platform/native/electron-main/nativeHostMainService.ts#L679-L771 import { KeytarService } from '../common/keytar-protocol'; -import { injectable } from 'inversify'; +import { inject, injectable } from 'inversify'; import { isWindows } from '../common'; -import * as keytar from 'keytar'; +import { BackendRemoteService } from './remote/backend-remote-service'; @injectable() export class KeytarServiceImpl implements KeytarService { + + @inject(BackendRemoteService) + protected readonly backendRemoteService: BackendRemoteService; + private static readonly MAX_PASSWORD_LENGTH = 2500; private static readonly PASSWORD_CHUNK_SIZE = KeytarServiceImpl.MAX_PASSWORD_LENGTH - 100; + protected cache?: typeof import('keytar'); + async setPassword(service: string, account: string, password: string): Promise { + const keytar = await this.getKeytar(); if (isWindows && password.length > KeytarServiceImpl.MAX_PASSWORD_LENGTH) { let index = 0; let chunk = 0; @@ -54,11 +61,13 @@ export class KeytarServiceImpl implements KeytarService { } } - deletePassword(service: string, account: string): Promise { + async deletePassword(service: string, account: string): Promise { + const keytar = await this.getKeytar(); return keytar.deletePassword(service, account); } async getPassword(service: string, account: string): Promise { + const keytar = await this.getKeytar(); const password = await keytar.getPassword(service, account); if (password) { try { @@ -81,15 +90,78 @@ export class KeytarServiceImpl implements KeytarService { } } } + async findPassword(service: string): Promise { + const keytar = await this.getKeytar(); const password = await keytar.findPassword(service); if (password) { return password; } } + async findCredentials(service: string): Promise> { + const keytar = await this.getKeytar(); return keytar.findCredentials(service); } + + protected async getKeytar(): Promise { + if (this.cache) { + return this.cache; + } + try { + return (this.cache = await import('keytar')); + } catch (err) { + if (this.backendRemoteService.isRemoteServer()) { + // When running as a remote server, the necessary prerequisites might not be installed on the system + // Just use an in-memory cache for credentials + return (this.cache = new InMemoryCredentialsProvider()); + } else { + throw err; + } + } + } +} + +export class InMemoryCredentialsProvider { + private secretVault: Record | undefined> = {}; + + async getPassword(service: string, account: string): Promise { + // eslint-disable-next-line no-null/no-null + return this.secretVault[service]?.[account] ?? null; + } + + async setPassword(service: string, account: string, password: string): Promise { + this.secretVault[service] = this.secretVault[service] ?? {}; + this.secretVault[service]![account] = password; + } + + async deletePassword(service: string, account: string): Promise { + if (!this.secretVault[service]?.[account]) { + return false; + } + delete this.secretVault[service]![account]; + if (Object.keys(this.secretVault[service]!).length === 0) { + delete this.secretVault[service]; + } + return true; + } + + async findPassword(service: string): Promise { + // eslint-disable-next-line no-null/no-null + return JSON.stringify(this.secretVault[service]) ?? null; + } + + async findCredentials(service: string): Promise> { + const credentials: { account: string; password: string }[] = []; + for (const account of Object.keys(this.secretVault[service] || {})) { + credentials.push({ account, password: this.secretVault[service]![account] }); + } + return credentials; + } + + async clear(): Promise { + this.secretVault = {}; + } } interface ChunkedPassword { diff --git a/packages/core/src/node/messaging/messaging-contribution.ts b/packages/core/src/node/messaging/messaging-contribution.ts index 635db758c3457..a6de18c22d315 100644 --- a/packages/core/src/node/messaging/messaging-contribution.ts +++ b/packages/core/src/node/messaging/messaging-contribution.ts @@ -60,11 +60,11 @@ export class MessagingContribution implements BackendApplicationContribution, Me } wsChannel(spec: string, callback: (params: MessagingService.PathParams, channel: Channel) => void): void { - this.channelHandlers.push(spec, (params, channel) => callback(params, channel)); + return this.channelHandlers.push(spec, (params, channel) => callback(params, channel)); } ws(spec: string, callback: (params: MessagingService.PathParams, socket: Socket) => void): void { - this.wsHandlers.push(spec, callback); + return this.wsHandlers.push(spec, callback); } protected checkAliveTimeout = 30000; // 30 seconds @@ -83,7 +83,7 @@ export class MessagingContribution implements BackendApplicationContribution, Me // We provide a `fix-origin` header in the `WebSocketConnectionProvider` request.headers.origin = request.headers['fix-origin'] as string; if (await this.allowConnect(socket.request)) { - this.handleConnection(socket); + await this.handleConnection(socket); this.messagingListener.onDidWebSocketUpgrade(socket.request, socket); } else { socket.disconnect(true); @@ -91,7 +91,7 @@ export class MessagingContribution implements BackendApplicationContribution, Me }); } - protected handleConnection(socket: Socket): void { + protected async handleConnection(socket: Socket): Promise { const pathname = socket.nsp.name; if (pathname && !this.wsHandlers.route(pathname, socket)) { console.error('Cannot find a ws handler for the path: ' + pathname); @@ -166,14 +166,15 @@ export namespace MessagingContribution { push(spec: string, callback: (params: MessagingService.PathParams, connection: T) => void): void { const route = new Route(spec); - this.handlers.push((path, channel) => { + const handler = (path: string, channel: T): string | false => { const params = route.match(path); if (!params) { return false; } callback(params, channel); return route.reverse(params); - }); + }; + this.handlers.push(handler); } route(path: string, connection: T): string | false { diff --git a/packages/core/src/node/remote/app-native-dependency-contribution.ts b/packages/core/src/node/remote/app-native-dependency-contribution.ts new file mode 100644 index 0000000000000..7cc0b8a401920 --- /dev/null +++ b/packages/core/src/node/remote/app-native-dependency-contribution.ts @@ -0,0 +1,38 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { injectable } from 'inversify'; +import { RemoteNativeDependencyContribution, DownloadOptions, DependencyDownload, RemotePlatform } from './remote-native-dependency-contribution'; + +@injectable() +export class AppNativeDependencyContribution implements RemoteNativeDependencyContribution { + + // TODO: Points for testing purposes to a non-theia repo + // Should be replaced with: + // 'https://github.com/eclipse-theia/theia/releases/download' + appDownloadUrlBase = 'https://github.com/msujew/theia/releases/download'; + + getDefaultURLForFile(remotePlatform: RemotePlatform, theiaVersion: string): string { + return `${this.appDownloadUrlBase}/v${theiaVersion}/native-dependencies-${remotePlatform}-x64.zip`; + } + + async download(options: DownloadOptions): Promise { + return { + buffer: await options.download(this.getDefaultURLForFile(options.remotePlatform, options.theiaVersion)), + archive: 'zip' + }; + } +} diff --git a/packages/core/src/node/remote/backend-remote-module.ts b/packages/core/src/node/remote/backend-remote-module.ts new file mode 100644 index 0000000000000..099f12c57a771 --- /dev/null +++ b/packages/core/src/node/remote/backend-remote-module.ts @@ -0,0 +1,35 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { ContainerModule } from 'inversify'; +import { bindContributionProvider } from '../../common'; +import { CoreCopyContribution } from './core-copy-contribution'; +import { RemoteCopyContribution, RemoteCopyRegistry } from './remote-copy-contribution'; +import { RemoteNativeDependencyContribution } from './remote-native-dependency-contribution'; +import { AppNativeDependencyContribution } from './app-native-dependency-contribution'; +import { BackendRemoteService } from './backend-remote-service'; + +export default new ContainerModule(bind => { + bind(BackendRemoteService).toSelf().inSingletonScope(); + bindContributionProvider(bind, RemoteCopyContribution); + bind(RemoteCopyRegistry).toSelf().inSingletonScope(); + bind(CoreCopyContribution).toSelf().inSingletonScope(); + bind(RemoteCopyContribution).toService(CoreCopyContribution); + + bindContributionProvider(bind, RemoteNativeDependencyContribution); + bind(AppNativeDependencyContribution).toSelf().inSingletonScope(); + bind(RemoteNativeDependencyContribution).toService(AppNativeDependencyContribution); +}); diff --git a/packages/core/src/node/remote/backend-remote-service.ts b/packages/core/src/node/remote/backend-remote-service.ts new file mode 100644 index 0000000000000..b898808533353 --- /dev/null +++ b/packages/core/src/node/remote/backend-remote-service.ts @@ -0,0 +1,25 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { injectable } from 'inversify'; + +@injectable() +export class BackendRemoteService { + + isRemoteServer(): boolean { + return false; + } +} diff --git a/packages/core/src/node/remote/core-copy-contribution.ts b/packages/core/src/node/remote/core-copy-contribution.ts new file mode 100644 index 0000000000000..4599acfadb767 --- /dev/null +++ b/packages/core/src/node/remote/core-copy-contribution.ts @@ -0,0 +1,28 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { injectable } from 'inversify'; +import { RemoteCopyContribution, RemoteCopyRegistry } from './remote-copy-contribution'; + +@injectable() +export class CoreCopyContribution implements RemoteCopyContribution { + async copy(registry: RemoteCopyRegistry): Promise { + registry.file('package.json'); + await registry.glob('lib/backend/!(native)'); + await registry.directory('lib/frontend'); + await registry.directory('lib/webview'); + } +} diff --git a/packages/core/src/node/remote/index.ts b/packages/core/src/node/remote/index.ts new file mode 100644 index 0000000000000..c8dd5ed896c43 --- /dev/null +++ b/packages/core/src/node/remote/index.ts @@ -0,0 +1,20 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +export * from './core-copy-contribution'; +export * from './remote-copy-contribution'; +export * from './remote-native-dependency-contribution'; +export * from './app-native-dependency-contribution'; diff --git a/packages/core/src/node/remote/remote-copy-contribution.ts b/packages/core/src/node/remote/remote-copy-contribution.ts new file mode 100644 index 0000000000000..e41afdf9011b1 --- /dev/null +++ b/packages/core/src/node/remote/remote-copy-contribution.ts @@ -0,0 +1,85 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { ApplicationPackage } from '@theia/application-package'; +import { inject, injectable } from 'inversify'; +import { glob as globCallback } from 'glob'; +import { MaybePromise } from '../../common'; +import { promisify } from 'util'; +import * as path from 'path'; + +const promiseGlob = promisify(globCallback); + +export const RemoteCopyContribution = Symbol('RemoteCopyContribution'); + +export interface RemoteCopyContribution { + copy(registry: RemoteCopyRegistry): MaybePromise +} + +export interface RemoteCopyOptions { + mode?: number; +} + +export interface RemoteFile { + path: string + target: string + options?: RemoteCopyOptions; +} + +@injectable() +export class RemoteCopyRegistry { + + @inject(ApplicationPackage) + protected readonly applicationPackage: ApplicationPackage; + + protected readonly files: RemoteFile[] = []; + + getFiles(): RemoteFile[] { + return this.files.slice(); + } + + async glob(pattern: string, target?: string): Promise { + const projectPath = this.applicationPackage.projectPath; + const globResult = await promiseGlob(pattern, { + cwd: projectPath + }); + const relativeFiles = globResult.map(file => path.relative(projectPath, file)); + for (const file of relativeFiles) { + const targetFile = this.withTarget(file, target); + this.files.push({ + path: file, + target: targetFile + }); + } + } + + file(file: string, target?: string, options?: RemoteCopyOptions): void { + const targetFile = this.withTarget(file, target); + this.files.push({ + path: file, + target: targetFile, + options + }); + } + + async directory(dir: string, target?: string): Promise { + return this.glob(dir + '/**', target); + } + + protected withTarget(file: string, target?: string): string { + return target ? path.join(target, file) : file; + } +} diff --git a/packages/core/src/node/remote/remote-native-dependency-contribution.ts b/packages/core/src/node/remote/remote-native-dependency-contribution.ts new file mode 100644 index 0000000000000..55d1c3deed36f --- /dev/null +++ b/packages/core/src/node/remote/remote-native-dependency-contribution.ts @@ -0,0 +1,71 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { RequestOptions } from '@theia/request'; +import { isObject } from '../../common'; + +export interface FileDependencyResult { + path: string; + mode?: number; +} + +export type RemotePlatform = 'windows' | 'linux' | 'darwin'; + +export namespace RemotePlatform { + export function joinPath(platform: RemotePlatform, ...segments: string[]): string { + const separator = platform === 'windows' ? '\\' : '/'; + return segments.join(separator); + } +} + +export type DependencyDownload = FileDependencyDownload | DirectoryDependencyDownload; + +export interface FileDependencyDownload { + file: FileDependencyResult + buffer: Buffer +} + +export namespace FileDependencyResult { + export function is(item: unknown): item is FileDependencyDownload { + return isObject(item) && 'buffer' in item && 'file' in item; + } +} + +export interface DirectoryDependencyDownload { + archive: 'tar' | 'zip' | 'tgz' + buffer: Buffer +} + +export namespace DirectoryDependencyDownload { + export function is(item: unknown): item is DirectoryDependencyDownload { + return isObject(item) && 'buffer' in item && 'archive' in item; + } +} + +export interface DownloadOptions { + remotePlatform: RemotePlatform; + theiaVersion: string; + download: (requestInfo: string | RequestOptions) => Promise +} + +export const RemoteNativeDependencyContribution = Symbol('RemoteNativeDependencyContribution'); + +/** + * contribution used for downloading prebuild nativ dependency when connecting to a remote machine with a different system + */ +export interface RemoteNativeDependencyContribution { + download(options: DownloadOptions): Promise; +} diff --git a/packages/filesystem/package.json b/packages/filesystem/package.json index c7d2ad782c88b..89fbdf53e95cb 100644 --- a/packages/filesystem/package.json +++ b/packages/filesystem/package.json @@ -38,7 +38,9 @@ "backend": "lib/node/download/file-download-backend-module" }, { - "frontend": "lib/browser/file-dialog/file-dialog-module", + "frontend": "lib/browser/file-dialog/file-dialog-module" + }, + { "frontendElectron": "lib/electron-browser/file-dialog/electron-file-dialog-module" } ], diff --git a/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-module.ts b/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-module.ts index 42b5dd29fb6f9..94a2381944296 100644 --- a/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-module.ts +++ b/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-module.ts @@ -18,7 +18,7 @@ import { ContainerModule } from '@theia/core/shared/inversify'; import { FileDialogService } from '../../browser/file-dialog/file-dialog-service'; import { ElectronFileDialogService } from './electron-file-dialog-service'; -export default new ContainerModule(bind => { +export default new ContainerModule((bind, _unbind, _isBound, rebind) => { bind(ElectronFileDialogService).toSelf().inSingletonScope(); - bind(FileDialogService).toService(ElectronFileDialogService); + rebind(FileDialogService).toService(ElectronFileDialogService); }); diff --git a/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts b/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts index abb9c57372cb8..829a83f6b2f9b 100644 --- a/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts +++ b/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts @@ -16,6 +16,7 @@ import { inject, injectable } from '@theia/core/shared/inversify'; import URI from '@theia/core/lib/common/uri'; +import { RemoteService } from '@theia/core/lib/browser/remote-service'; import { MaybeArray } from '@theia/core/lib/common/types'; import { MessageService } from '@theia/core/lib/common/message-service'; import { FileStat } from '../../common/files'; @@ -35,10 +36,14 @@ import { OpenDialogOptions, SaveDialogOptions } from '../../electron-common/elec export class ElectronFileDialogService extends DefaultFileDialogService { @inject(MessageService) protected readonly messageService: MessageService; + @inject(RemoteService) protected readonly remoteService: RemoteService; override async showOpenDialog(props: OpenFileDialogProps & { canSelectMany: true }, folder?: FileStat): Promise | undefined>; override async showOpenDialog(props: OpenFileDialogProps, folder?: FileStat): Promise; override async showOpenDialog(props: OpenFileDialogProps, folder?: FileStat): Promise | undefined> { + if (this.remoteService.isConnected()) { + return super.showOpenDialog(props, folder); + } const rootNode = await this.getRootNode(folder); if (rootNode) { const filePaths = await window.electronTheiaFilesystem.showOpenDialog(this.toOpenDialogOptions(rootNode.uri, props)); @@ -55,6 +60,9 @@ export class ElectronFileDialogService extends DefaultFileDialogService { } override async showSaveDialog(props: SaveFileDialogProps, folder?: FileStat): Promise { + if (this.remoteService.isConnected()) { + return super.showSaveDialog(props, folder); + } const rootNode = await this.getRootNode(folder); if (rootNode) { const filePath = await window.electronTheiaFilesystem.showSaveDialog(this.toSaveDialogOptions(rootNode.uri, props)); diff --git a/packages/monaco/src/browser/monaco-frontend-module.ts b/packages/monaco/src/browser/monaco-frontend-module.ts index bf384e0d51a8d..e688a356f8cef 100644 --- a/packages/monaco/src/browser/monaco-frontend-module.ts +++ b/packages/monaco/src/browser/monaco-frontend-module.ts @@ -32,11 +32,11 @@ Object.assign(MonacoNls, { import '../../src/browser/style/index.css'; import { ContainerModule, decorate, injectable, interfaces } from '@theia/core/shared/inversify'; -import { MenuContribution, CommandContribution } from '@theia/core/lib/common'; +import { MenuContribution, CommandContribution, quickInputServicePath } from '@theia/core/lib/common'; import { FrontendApplicationContribution, KeybindingContribution, PreferenceService, PreferenceSchemaProvider, createPreferenceProxy, - PreferenceScope, PreferenceChange, OVERRIDE_PROPERTY_PATTERN, QuickInputService, StylingParticipant + PreferenceScope, PreferenceChange, OVERRIDE_PROPERTY_PATTERN, QuickInputService, StylingParticipant, WebSocketConnectionProvider } from '@theia/core/lib/browser'; import { TextEditorProvider, DiffNavigatorProvider, TextEditor } from '@theia/editor/lib/browser'; import { MonacoEditorProvider, MonacoEditorFactory } from './monaco-editor-provider'; @@ -159,7 +159,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(KeybindingContribution).toService(MonacoKeybindingContribution); bind(MonacoQuickInputImplementation).toSelf().inSingletonScope(); - bind(MonacoQuickInputService).toSelf().inSingletonScope(); + bind(MonacoQuickInputService).toSelf().inSingletonScope().onActivation(({ container }, quickInputService: MonacoQuickInputService) => { + WebSocketConnectionProvider.createDualProxy(container, quickInputServicePath, quickInputService); + return quickInputService; + }); bind(QuickInputService).toService(MonacoQuickInputService); bind(MonacoQuickAccessRegistry).toSelf().inSingletonScope(); diff --git a/packages/plugin-ext/src/main/node/plugin-service.ts b/packages/plugin-ext/src/main/node/plugin-service.ts index b5ef8158ee45e..a01504c335860 100644 --- a/packages/plugin-ext/src/main/node/plugin-service.ts +++ b/packages/plugin-ext/src/main/node/plugin-service.ts @@ -26,6 +26,7 @@ import { environment } from '@theia/core/shared/@theia/application-package/lib/e import { WsRequestValidatorContribution } from '@theia/core/lib/node/ws-request-validators'; import { MaybePromise } from '@theia/core/lib/common'; import { ApplicationPackage } from '@theia/core/shared/@theia/application-package'; +import { BackendRemoteService } from '@theia/core/lib/node/remote/backend-remote-service'; @injectable() export class PluginApiContribution implements BackendApplicationContribution, WsRequestValidatorContribution { @@ -37,6 +38,9 @@ export class PluginApiContribution implements BackendApplicationContribution, Ws @inject(ApplicationPackage) protected readonly applicationPackage: ApplicationPackage; + @inject(BackendRemoteService) + protected readonly remoteService: BackendRemoteService; + @postConstruct() protected init(): void { const webviewExternalEndpoint = this.webviewExternalEndpoint(); @@ -47,7 +51,13 @@ export class PluginApiContribution implements BackendApplicationContribution, Ws configure(app: express.Application): void { const webviewApp = express(); webviewApp.use('/webview', express.static(path.join(this.applicationPackage.projectPath, 'lib', 'webview', 'pre'))); - app.use(vhost(this.webviewExternalEndpointRegExp, webviewApp)); + if (this.remoteService.isRemoteServer()) { + // If we are a remote server, the subdomain information gets lost + // We simply serve the webviews on a path + app.use(webviewApp); + } else { + app.use(vhost(this.webviewExternalEndpointRegExp, webviewApp)); + } } allowWsUpgrade(request: http.IncomingMessage): MaybePromise { diff --git a/packages/remote/.eslintrc.js b/packages/remote/.eslintrc.js new file mode 100644 index 0000000000000..13089943582b6 --- /dev/null +++ b/packages/remote/.eslintrc.js @@ -0,0 +1,10 @@ +/** @type {import('eslint').Linter.Config} */ +module.exports = { + extends: [ + '../../configs/build.eslintrc.json' + ], + parserOptions: { + tsconfigRootDir: __dirname, + project: 'tsconfig.json' + } +}; diff --git a/packages/remote/README.md b/packages/remote/README.md new file mode 100644 index 0000000000000..3444927144483 --- /dev/null +++ b/packages/remote/README.md @@ -0,0 +1,60 @@ +
+ +
+ +theia-ext-logo + +

ECLIPSE THEIA - REMOTE EXTENSION

+ +
+ +
+ +## Description + +This package implements functionality to connect to remote systems using Theia. +This facilitates features similar to the features offered by Microsoft's popular `Remote-SSH`, `Dev Containers` or `WSL` extensions for VSCode. + +## Package Architecture + +The following explains the basic flow of any remote connection. It will be exemplified using the remote SSH feature: + +1. When the user runs the `SSH: Connect to Host...` command, we send the host info to the local backend. +The corresponding `RemoteSSHConnectionProvider` is scoped to the current connection and can request additional information from the user, such as SSH key passphrases. +2. Once the `RemoteSSHConnectionProvider` has every information it needs, it creates a SSH connection and registers this connection to the general `RemoteConnectionService`. +Every `RemoteConnection` type implements an interface that is able to handle 3 kinds of messages to the remote system: + 1. Executing commands in the shell of the remote system + 2. Copying data to the remote +3. Once the connection has been established, a setup process takes place on the remote system: + 1. Idenfying the remote platform (i.e. Windows, MacOS or Linux). This information is needed for all the following steps. + 2. Setting up various directories for storing the application and its dependencies. + 3. Download and install the correct Node.js version for the remote platform. + 4. Packaging, copying, and unpackaging the local backend to the remote backend. + 1. Every Theia extension can register `RemoteCopyContribution` binding to copy certain files from the current system. + This contribution point is used for files that are used in all operating systems. + 2. They can also register `RemoteNativeDependencyContribution` bindings to download and copy native dependencies for the remote system. + The downloaded files are on a per-platform basis. + 5. Using the node version that was installed in step 3, we now start the `main.js` of the backend application. + We start the backend with `--port=0`, so that it searches for any available port. It will print the port to the console. + The setup either returns with a setup error or the port of the remote server on the remote system. +4. With the remote server/port in place, the backend sets up a local proxy server on a random port. +It instructs the `RemoteConnection` object to forward any HTTP request to this proxy server to the remote server. +5. The backend will return from the initial request from (1) with a new local proxy port. The frontend sets this port in the url and reload itself. +6. The frontend is now connected to the remote backend by connecting to the local proxy port. +7. The frontend now performs its normal messaging lifecycle, establishing connections to backend services. +Although these backend services live on a different remote system, the frontend handles them as if they belong to the local backend. + +## Additional Information + +- [API documentation for `@theia/remote`](https://eclipse-theia.github.io/theia/docs/next/modules/remote.html) +- [Theia - GitHub](https://github.com/eclipse-theia/theia) +- [Theia - Website](https://theia-ide.org/) + +## License + +- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/) +- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp) + +## Trademark +"Theia" is a trademark of the Eclipse Foundation +https://www.eclipse.org/theia diff --git a/packages/remote/package.json b/packages/remote/package.json new file mode 100644 index 0000000000000..b841e6a1dd188 --- /dev/null +++ b/packages/remote/package.json @@ -0,0 +1,64 @@ +{ + "name": "@theia/remote", + "version": "1.42.0", + "description": "Theia - Remote", + "dependencies": { + "@theia/core": "1.42.0", + "archiver": "^5.3.1", + "decompress": "^4.2.1", + "decompress-tar": "^4.0.0", + "decompress-targz": "^4.0.0", + "decompress-unzip": "^4.0.1", + "express-http-proxy": "^1.6.3", + "nanoid": "3.3.4", + "ssh2": "^1.12.0", + "ssh2-sftp-client": "^9.1.0", + "socket.io": "^4.5.3", + "socket.io-client": "^4.5.3", + "uuid": "^8.0.0" + }, + "publishConfig": { + "access": "public" + }, + "theiaExtensions": [ + { + "frontendElectron": "lib/electron-browser/remote-frontend-module", + "backendElectron": "lib/electron-node/remote-backend-module" + } + ], + "keywords": [ + "theia-extension" + ], + "license": "EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0", + "repository": { + "type": "git", + "url": "https://github.com/theia-ide/theia.git" + }, + "bugs": { + "url": "https://github.com/theia-ide/theia/issues" + }, + "homepage": "https://github.com/theia-ide/theia", + "files": [ + "lib", + "src" + ], + "scripts": { + "build": "theiaext build", + "clean": "theiaext clean", + "compile": "theiaext compile", + "lint": "theiaext lint", + "test": "theiaext test", + "watch": "theiaext watch" + }, + "devDependencies": { + "@theia/ext-scripts": "1.42.0", + "@types/archiver": "^5.3.2", + "@types/decompress": "^4.2.4", + "@types/express-http-proxy": "^1.6.3", + "@types/ssh2": "^1.11.11", + "@types/ssh2-sftp-client": "^9.0.0" + }, + "nyc": { + "extends": "../../configs/nyc.json" + } +} diff --git a/packages/remote/src/electron-browser/remote-frontend-contribution.ts b/packages/remote/src/electron-browser/remote-frontend-contribution.ts new file mode 100644 index 0000000000000..40734b356e20b --- /dev/null +++ b/packages/remote/src/electron-browser/remote-frontend-contribution.ts @@ -0,0 +1,145 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { Command, CommandContribution, CommandRegistry, ContributionProvider, nls, QuickInputService, QuickPickInput } from '@theia/core'; +import { FrontendApplicationContribution, StatusBar, StatusBarAlignment, StatusBarEntry } from '@theia/core/lib/browser'; +import { inject, injectable, named, optional } from '@theia/core/shared/inversify'; +import { RemoteStatus, RemoteStatusService } from '../electron-common/remote-status-service'; +import { RemoteRegistry, RemoteRegistryContribution } from './remote-registry-contribution'; +import { RemoteServiceImpl } from './remote-service-impl'; +import { WindowService } from '@theia/core/lib/browser/window/window-service'; + +export namespace RemoteCommands { + export const REMOTE_SELECT: Command = { + id: 'remote.select' + }; + export const REMOTE_DISCONNECT: Command = Command.toDefaultLocalizedCommand({ + id: 'remote.disconnect', + label: 'Close Remote Connection', + }); +} + +@injectable() +export class RemoteFrontendContribution implements CommandContribution, FrontendApplicationContribution { + + @inject(StatusBar) + protected readonly statusBar: StatusBar; + + @inject(QuickInputService) @optional() + protected readonly quickInputService?: QuickInputService; + + @inject(CommandRegistry) + protected readonly commandRegistry: CommandRegistry; + + @inject(RemoteServiceImpl) + protected readonly remoteService: RemoteServiceImpl; + + @inject(RemoteStatusService) + protected readonly remoteStatusService: RemoteStatusService; + + @inject(WindowService) + protected readonly windowService: WindowService; + + @inject(ContributionProvider) @named(RemoteRegistryContribution) + protected readonly remoteRegistryContributions: ContributionProvider; + + protected remoteRegistry = new RemoteRegistry(); + + async configure(): Promise { + const port = new URLSearchParams(location.search).get('port'); + if (port) { + const status = await this.remoteStatusService.getStatus(Number(port)); + await this.setStatusBar(status); + } else { + await this.setStatusBar({ + alive: false + }); + } + } + + protected async setStatusBar(info: RemoteStatus): Promise { + this.remoteService.connected = info.alive; + const entry: StatusBarEntry = { + alignment: StatusBarAlignment.LEFT, + command: RemoteCommands.REMOTE_SELECT.id, + backgroundColor: 'var(--theia-statusBarItem-remoteBackground)', + color: 'var(--theia-statusBarItem-remoteForeground)', + priority: 10000, + ...(info.alive + ? { + text: `$(codicon-remote) ${info.type}: ${info.name.length > 35 ? info.name.substring(0, 32) + '...' : info.name}`, + tooltip: nls.localizeByDefault('Editing on {0}', info.name), + } : { + text: '$(codicon-remote)', + tooltip: nls.localizeByDefault('Open a Remote Window'), + }) + }; + this.statusBar.setElement('remoteInfo', entry); + } + + registerCommands(commands: CommandRegistry): void { + this.remoteRegistry.onDidRegisterCommand(([command, handler]) => { + commands.registerCommand(command, handler); + }); + for (const contribution of this.remoteRegistryContributions.getContributions()) { + contribution.registerRemoteCommands(this.remoteRegistry); + } + commands.registerCommand(RemoteCommands.REMOTE_SELECT, { + execute: () => this.selectRemote() + }); + commands.registerCommand(RemoteCommands.REMOTE_DISCONNECT, { + execute: () => this.disconnectRemote() + }); + } + + protected disconnectRemote(): void { + const port = new URLSearchParams(location.search).get('localPort'); + if (port) { + this.windowService.reload({ + port + }); + } + } + + protected async selectRemote(): Promise { + const commands = [...this.remoteRegistry.commands]; + if (this.remoteService.isConnected()) { + commands.push(RemoteCommands.REMOTE_DISCONNECT); + } + const quickPicks: QuickPickInput[] = []; + let previousCategory: string | undefined = undefined; + for (const command of commands) { + if (previousCategory !== command.category) { + quickPicks.push({ + type: 'separator', + label: command.category + }); + previousCategory = command.category; + } + quickPicks.push({ + label: command.label!, + id: command.id + }); + } + const selection = await this.quickInputService?.showQuickPick(quickPicks, { + placeholder: nls.localizeByDefault('Select an option to open a Remote Window') + }); + if (selection) { + this.commandRegistry.executeCommand(selection.id!); + } + } + +} diff --git a/packages/remote/src/electron-browser/remote-frontend-module.ts b/packages/remote/src/electron-browser/remote-frontend-module.ts new file mode 100644 index 0000000000000..ceae6edd6ee45 --- /dev/null +++ b/packages/remote/src/electron-browser/remote-frontend-module.ts @@ -0,0 +1,43 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { bindContributionProvider, CommandContribution } from '@theia/core'; +import { ContainerModule } from '@theia/core/shared/inversify'; +import { FrontendApplicationContribution, RemoteService, WebSocketConnectionProvider } from '@theia/core/lib/browser'; +import { RemoteSSHContribution } from './remote-ssh-contribution'; +import { RemoteSSHConnectionProvider, RemoteSSHConnectionProviderPath } from '../electron-common/remote-ssh-connection-provider'; +import { RemoteFrontendContribution } from './remote-frontend-contribution'; +import { RemoteRegistryContribution } from './remote-registry-contribution'; +import { RemoteServiceImpl } from './remote-service-impl'; +import { RemoteStatusService, RemoteStatusServicePath } from '../electron-common/remote-status-service'; + +export default new ContainerModule((bind, _, __, rebind) => { + bind(RemoteFrontendContribution).toSelf().inSingletonScope(); + bind(FrontendApplicationContribution).toService(RemoteFrontendContribution); + bind(CommandContribution).toService(RemoteFrontendContribution); + + bindContributionProvider(bind, RemoteRegistryContribution); + bind(RemoteSSHContribution).toSelf().inSingletonScope(); + bind(RemoteRegistryContribution).toService(RemoteSSHContribution); + + bind(RemoteServiceImpl).toSelf().inSingletonScope(); + rebind(RemoteService).toService(RemoteServiceImpl); + + bind(RemoteSSHConnectionProvider).toDynamicValue(ctx => + WebSocketConnectionProvider.createLocalProxy(ctx.container, RemoteSSHConnectionProviderPath)).inSingletonScope(); + bind(RemoteStatusService).toDynamicValue(ctx => + WebSocketConnectionProvider.createLocalProxy(ctx.container, RemoteStatusServicePath)).inSingletonScope(); +}); diff --git a/packages/remote/src/electron-browser/remote-registry-contribution.ts b/packages/remote/src/electron-browser/remote-registry-contribution.ts new file mode 100644 index 0000000000000..c936aeb833886 --- /dev/null +++ b/packages/remote/src/electron-browser/remote-registry-contribution.ts @@ -0,0 +1,70 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { Command, CommandHandler, Emitter, Event } from '@theia/core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { WindowService } from '@theia/core/lib/browser/window/window-service'; +import { WindowSearchParams } from '@theia/core/lib/common/window'; + +export const RemoteRegistryContribution = Symbol('RemoteRegistryContribution'); + +export interface RemoteRegistryContribution { + registerRemoteCommands(registry: RemoteRegistry): void; +} + +@injectable() +export abstract class AbstractRemoteRegistryContribution implements RemoteRegistryContribution { + + @inject(WindowService) + protected readonly windowService: WindowService; + + abstract registerRemoteCommands(registry: RemoteRegistry): void; + + protected openRemote(port: string, newWindow: boolean): void { + const searchParams = new URLSearchParams(location.search); + const localPort = searchParams.get('localPort') || searchParams.get('port'); + const options: WindowSearchParams = { + port + }; + if (localPort) { + options.localPort = localPort; + } + if (newWindow) { + this.windowService.openNewDefaultWindow(options); + } else { + this.windowService.reload(options); + } + } +} + +export class RemoteRegistry { + + protected _commands: Command[] = []; + protected onDidRegisterCommandEmitter = new Emitter<[Command, CommandHandler | undefined]>(); + + get commands(): readonly Command[] { + return this._commands; + } + + get onDidRegisterCommand(): Event<[Command, CommandHandler | undefined]> { + return this.onDidRegisterCommandEmitter.event; + } + + registerCommand(command: Command, handler?: CommandHandler): void { + this.onDidRegisterCommandEmitter.fire([command, handler]); + this._commands.push(command); + } +} diff --git a/packages/remote/src/electron-browser/remote-service-impl.ts b/packages/remote/src/electron-browser/remote-service-impl.ts new file mode 100644 index 0000000000000..67689456e968a --- /dev/null +++ b/packages/remote/src/electron-browser/remote-service-impl.ts @@ -0,0 +1,28 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { RemoteService } from '@theia/core/lib/browser'; +import { injectable } from '@theia/core/shared/inversify'; + +@injectable() +export class RemoteServiceImpl implements RemoteService { + + connected: boolean; + + isConnected(): boolean { + return this.connected; + } +} diff --git a/packages/remote/src/electron-browser/remote-ssh-contribution.ts b/packages/remote/src/electron-browser/remote-ssh-contribution.ts new file mode 100644 index 0000000000000..4367fd0ea919c --- /dev/null +++ b/packages/remote/src/electron-browser/remote-ssh-contribution.ts @@ -0,0 +1,86 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { Command, MessageService, nls, QuickInputService } from '@theia/core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { RemoteSSHConnectionProvider } from '../electron-common/remote-ssh-connection-provider'; +import { AbstractRemoteRegistryContribution, RemoteRegistry } from './remote-registry-contribution'; + +export namespace RemoteSSHCommands { + export const CONNECT: Command = Command.toLocalizedCommand({ + id: 'remote.ssh.connect', + category: 'SSH', + label: 'Connect to Host...', + }, 'theia/remoteSSH/connect'); + export const CONNECT_CURRENT_WINDOW: Command = Command.toLocalizedCommand({ + id: 'remote.ssh.connectCurrentWindow', + category: 'SSH', + label: 'Connect Current Window to Host...', + }, 'theia/remoteSSH/connect'); +} + +@injectable() +export class RemoteSSHContribution extends AbstractRemoteRegistryContribution { + + @inject(QuickInputService) + protected readonly quickInputService: QuickInputService; + + @inject(RemoteSSHConnectionProvider) + protected readonly sshConnectionProvider: RemoteSSHConnectionProvider; + + @inject(MessageService) + protected readonly messageService: MessageService; + + registerRemoteCommands(registry: RemoteRegistry): void { + registry.registerCommand(RemoteSSHCommands.CONNECT, { + execute: () => this.connect(true) + }); + registry.registerCommand(RemoteSSHCommands.CONNECT_CURRENT_WINDOW, { + execute: () => this.connect(false) + }); + } + + async connect(newWindow: boolean): Promise { + let host: string | undefined; + let user: string | undefined; + host = await this.requestQuickInput('host'); + if (host?.includes('@')) { + const split = host.split('@'); + user = split[0]; + host = split[1]; + } + if (!user) { + user = await this.requestQuickInput('user'); + } + + try { + const remotePort = await this.sendSSHConnect(host!, user!); + this.openRemote(remotePort, newWindow); + } catch (err) { + this.messageService.error(`${nls.localize('theia/remote/sshFailure', 'Could not open SSH connection to remote.')} ${err.message ?? String(err)}`); + } + } + + async requestQuickInput(prompt: string): Promise { + return this.quickInputService.input({ + prompt + }); + } + + async sendSSHConnect(host: string, user: string): Promise { + return this.sshConnectionProvider.establishConnection(host, user); + } +} diff --git a/packages/remote/src/electron-common/remote-ssh-connection-provider.ts b/packages/remote/src/electron-common/remote-ssh-connection-provider.ts new file mode 100644 index 0000000000000..c1e7650151feb --- /dev/null +++ b/packages/remote/src/electron-common/remote-ssh-connection-provider.ts @@ -0,0 +1,29 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +export const RemoteSSHConnectionProviderPath = '/remote/ssh'; + +export const RemoteSSHConnectionProvider = Symbol('RemoteSSHConnectionProvider'); + +export interface RemoteSSHConnectionOptions { + user?: string; + host?: string; +} + +export interface RemoteSSHConnectionProvider { + establishConnection(host: string, user: string): Promise; + isConnectionAlive(remoteId: string): Promise; +} diff --git a/packages/remote/src/electron-common/remote-status-service.ts b/packages/remote/src/electron-common/remote-status-service.ts new file mode 100644 index 0000000000000..a4ccec7376156 --- /dev/null +++ b/packages/remote/src/electron-common/remote-status-service.ts @@ -0,0 +1,35 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +export type RemoteStatus = RemoteConnectedStatus | RemoteDisconnectedStatus; + +export interface RemoteDisconnectedStatus { + alive: false; +} + +export interface RemoteConnectedStatus { + alive: true; + type: string; + name: string; +} + +export const RemoteStatusServicePath = '/remote/status'; + +export const RemoteStatusService = Symbol('RemoteStatusService'); + +export interface RemoteStatusService { + getStatus(localPort: number): Promise +} diff --git a/packages/remote/src/electron-node/backend-remote-service-impl.ts b/packages/remote/src/electron-node/backend-remote-service-impl.ts new file mode 100644 index 0000000000000..db88035eeb4ef --- /dev/null +++ b/packages/remote/src/electron-node/backend-remote-service-impl.ts @@ -0,0 +1,45 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { CliContribution } from '@theia/core/lib/node'; +import { injectable } from '@theia/core/shared/inversify'; +import { Arguments, Argv } from '@theia/core/shared/yargs'; +import { BackendRemoteService } from '@theia/core/lib/node/remote/backend-remote-service'; + +export const REMOTE_START = 'remote'; + +@injectable() +export class BackendRemoteServiceImpl extends BackendRemoteService implements CliContribution { + + protected isRemote: boolean = false; + + configure(conf: Argv): void { + conf.option(REMOTE_START, { + description: 'Starts the server as an endpoint for a remote connection (i.e. through SSH)', + type: 'boolean', + default: false + }); + } + + setArguments(args: Arguments): void { + this.isRemote = Boolean(args[REMOTE_START]); + } + + override isRemoteServer(): boolean { + return this.isRemote; + } + +} diff --git a/packages/remote/src/electron-node/remote-backend-module.ts b/packages/remote/src/electron-node/remote-backend-module.ts new file mode 100644 index 0000000000000..f332b14383d96 --- /dev/null +++ b/packages/remote/src/electron-node/remote-backend-module.ts @@ -0,0 +1,68 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { ContainerModule } from '@theia/core/shared/inversify'; +import { BackendApplicationContribution, CliContribution } from '@theia/core/lib/node'; +import { RemoteConnectionService } from './remote-connection-service'; +import { RemoteProxyServerProvider } from './remote-proxy-server-provider'; +import { RemoteConnectionSocketProvider } from './remote-connection-socket-provider'; +import { RemoteTunnelService } from './remote-tunnel-service'; +import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connection-container-module'; +import { RemoteSSHConnectionProvider, RemoteSSHConnectionProviderPath } from '../electron-common/remote-ssh-connection-provider'; +import { RemoteSSHConnectionProviderImpl } from './ssh/remote-ssh-connection-provider'; +import { SSHIdentityFileCollector } from './ssh/ssh-identity-file-collector'; +import { RemoteCopyService } from './setup/remote-copy-service'; +import { RemoteSetupService } from './setup/remote-setup-service'; +import { RemoteNativeDependencyService } from './setup/remote-native-dependency-service'; +import { BackendRemoteServiceImpl } from './backend-remote-service-impl'; +import { BackendRemoteService } from '@theia/core/lib/node/remote/backend-remote-service'; +import { RemoteNodeSetupService } from './setup/remote-node-setup-service'; +import { RemoteSetupScriptService } from './setup/remote-setup-script-service'; +import { RemoteStatusService, RemoteStatusServicePath } from '../electron-common/remote-status-service'; +import { RemoteStatusServiceImpl } from './remote-status-service'; +import { ConnectionHandler, RpcConnectionHandler } from '@theia/core'; + +export const remoteConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => { + bind(RemoteSSHConnectionProviderImpl).toSelf().inSingletonScope(); + bind(RemoteSSHConnectionProvider).toService(RemoteSSHConnectionProviderImpl); + bindBackendService(RemoteSSHConnectionProviderPath, RemoteSSHConnectionProvider); +}); + +export default new ContainerModule((bind, _unbind, _isBound, rebind) => { + bind(RemoteCopyService).toSelf().inSingletonScope(); + bind(RemoteSetupService).toSelf().inSingletonScope(); + bind(RemoteNodeSetupService).toSelf().inSingletonScope(); + bind(RemoteSetupScriptService).toSelf().inSingletonScope(); + bind(RemoteNativeDependencyService).toSelf().inSingletonScope(); + bind(RemoteProxyServerProvider).toSelf().inSingletonScope(); + bind(RemoteConnectionSocketProvider).toSelf().inSingletonScope(); + bind(RemoteTunnelService).toSelf().inSingletonScope(); + bind(RemoteConnectionService).toSelf().inSingletonScope(); + bind(BackendApplicationContribution).toService(RemoteConnectionService); + bind(RemoteStatusServiceImpl).toSelf().inSingletonScope(); + bind(RemoteStatusService).toService(RemoteStatusServiceImpl); + bind(ConnectionHandler).toDynamicValue( + ctx => new RpcConnectionHandler(RemoteStatusServicePath, () => ctx.container.get(RemoteStatusService)) + ).inSingletonScope(); + + bind(ConnectionContainerModule).toConstantValue(remoteConnectionModule); + + bind(BackendRemoteServiceImpl).toSelf().inSingletonScope(); + rebind(BackendRemoteService).toService(BackendRemoteServiceImpl); + bind(CliContribution).toService(BackendRemoteServiceImpl); + + bind(SSHIdentityFileCollector).toSelf().inSingletonScope(); +}); diff --git a/packages/remote/src/electron-node/remote-connection-service.ts b/packages/remote/src/electron-node/remote-connection-service.ts new file mode 100644 index 0000000000000..9e30189f693c3 --- /dev/null +++ b/packages/remote/src/electron-node/remote-connection-service.ts @@ -0,0 +1,60 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { inject, injectable } from '@theia/core/shared/inversify'; +import { RemoteConnection } from './remote-types'; +import { nanoid } from 'nanoid'; +import { Disposable } from '@theia/core'; +import { RemoteCopyService } from './setup/remote-copy-service'; +import { RemoteNativeDependencyService } from './setup/remote-native-dependency-service'; +import { BackendApplicationContribution } from '@theia/core/lib/node'; + +@injectable() +export class RemoteConnectionService implements BackendApplicationContribution { + + @inject(RemoteCopyService) + protected readonly copyService: RemoteCopyService; + + @inject(RemoteNativeDependencyService) + protected readonly nativeDependencyService: RemoteNativeDependencyService; + + protected readonly connections = new Map(); + + getConnection(id: string): RemoteConnection | undefined { + return this.connections.get(id); + } + + getConnectionFromPort(port: number): RemoteConnection | undefined { + return Array.from(this.connections.values()).find(connection => connection.localPort === port); + } + + getConnectionId(): string { + return nanoid(10); + } + + register(connection: RemoteConnection): Disposable { + this.connections.set(connection.id, connection); + return Disposable.create(() => { + this.connections.delete(connection.id); + }); + } + + onStop(): void { + for (const connection of this.connections.values()) { + connection.dispose(); + } + } +} diff --git a/packages/remote/src/electron-node/remote-connection-socket-provider.ts b/packages/remote/src/electron-node/remote-connection-socket-provider.ts new file mode 100644 index 0000000000000..7b65e45d89846 --- /dev/null +++ b/packages/remote/src/electron-node/remote-connection-socket-provider.ts @@ -0,0 +1,34 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { injectable } from '@theia/core/shared/inversify'; +import { io, Socket } from 'socket.io-client'; + +export interface RemoteProxySocketProviderOptions { + port: number; + path: string; +} + +@injectable() +export class RemoteConnectionSocketProvider { + + getProxySocket(options: RemoteProxySocketProviderOptions): Socket { + const socket = io(`ws://localhost:${options.port}${options.path}`); + socket.connect(); + return socket; + } + +} diff --git a/packages/remote/src/electron-node/remote-proxy-server-provider.ts b/packages/remote/src/electron-node/remote-proxy-server-provider.ts new file mode 100644 index 0000000000000..6dcc6607e7f58 --- /dev/null +++ b/packages/remote/src/electron-node/remote-proxy-server-provider.ts @@ -0,0 +1,37 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { Deferred } from '@theia/core/lib/common/promise-util'; +import { injectable } from '@theia/core/shared/inversify'; +import * as net from 'net'; + +@injectable() +export class RemoteProxyServerProvider { + + async getProxyServer(callback?: (socket: net.Socket) => void): Promise { + const deferred = new Deferred(); + + const proxy = net.createServer(socket => { + callback?.(socket); + }).listen(0, () => { + deferred.resolve(); + }); + + await deferred.promise; + return proxy; + } + +} diff --git a/packages/remote/src/electron-node/remote-status-service.ts b/packages/remote/src/electron-node/remote-status-service.ts new file mode 100644 index 0000000000000..a88f75c8862b0 --- /dev/null +++ b/packages/remote/src/electron-node/remote-status-service.ts @@ -0,0 +1,41 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { inject, injectable } from '@theia/core/shared/inversify'; +import { RemoteStatus, RemoteStatusService } from '../electron-common/remote-status-service'; +import { RemoteConnectionService } from './remote-connection-service'; + +@injectable() +export class RemoteStatusServiceImpl implements RemoteStatusService { + + @inject(RemoteConnectionService) + protected remoteConnectionService: RemoteConnectionService; + + async getStatus(localPort: number): Promise { + const connection = this.remoteConnectionService.getConnectionFromPort(localPort); + if (connection) { + return { + alive: true, + name: connection.name, + type: connection.type + }; + } else { + return { + alive: false + }; + } + } +} diff --git a/packages/remote/src/electron-node/remote-tunnel-service.ts b/packages/remote/src/electron-node/remote-tunnel-service.ts new file mode 100644 index 0000000000000..50dd89c0abaed --- /dev/null +++ b/packages/remote/src/electron-node/remote-tunnel-service.ts @@ -0,0 +1,56 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { inject, injectable } from '@theia/core/shared/inversify'; +import * as net from 'net'; +import { RemoteConnectionService } from './remote-connection-service'; +import { RemoteProxyServerProvider } from './remote-proxy-server-provider'; +import { RemoteTunnel } from './remote-types'; + +export interface RemoteTunnelOptions { + remote: string; +} + +@injectable() +export class RemoteTunnelService { + + @inject(RemoteConnectionService) + protected readonly connectionService: RemoteConnectionService; + + @inject(RemoteProxyServerProvider) + protected readonly serverProvider: RemoteProxyServerProvider; + + async addTunnel(options: RemoteTunnelOptions): Promise { + const connection = this.connectionService.getConnection(options.remote); + if (!connection) { + throw new Error('No remote connection found for id ' + options.remote); + } + const server = await this.serverProvider.getProxyServer(socket => connection.forwardOut(socket)); + const port = (server.address() as net.AddressInfo).port; + const tunnel = new RemoteTunnel({ + port + }); + // When the frontend socket disconnects, close the server + tunnel.onDidSocketDisconnect(() => { + server.close(); + }); + connection.onDidDisconnect(() => { + tunnel?.disconnect(); + }); + return tunnel; + } + +} diff --git a/packages/remote/src/electron-node/remote-types.ts b/packages/remote/src/electron-node/remote-types.ts new file mode 100644 index 0000000000000..0e93e33ea3790 --- /dev/null +++ b/packages/remote/src/electron-node/remote-types.ts @@ -0,0 +1,83 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { Disposable, Emitter, Event } from '@theia/core'; +import * as net from 'net'; + +export type RemoteStatusReport = (message: string) => void; + +export interface ExpressLayer { + name: string + regexp: RegExp + handle: Function + path?: string +} + +export interface RemoteExecOptions { + env?: NodeJS.ProcessEnv; +} + +export interface RemoteExecResult { + stdout: string; + stderr: string; +} + +export type RemoteExecTester = (stdout: string, stderr: string) => boolean; + +export interface RemoteConnection extends Disposable { + id: string; + name: string; + type: string; + localPort: number; + remotePort: number; + onDidDisconnect: Event; + forwardOut(socket: net.Socket): void; + exec(cmd: string, args?: string[], options?: RemoteExecOptions): Promise; + execPartial(cmd: string, tester: RemoteExecTester, args?: string[], options?: RemoteExecOptions): Promise; + copy(localPath: string | Buffer | NodeJS.ReadableStream, remotePath: string): Promise; +} + +export interface RemoteSessionOptions { + port: number; +} + +export class RemoteTunnel implements Disposable { + + readonly port: number; + + private readonly onDidRemoteDisconnectEmitter = new Emitter(); + private readonly onDidSocketDisconnectEmitter = new Emitter(); + + get onDidRemoteDisconnect(): Event { + return this.onDidRemoteDisconnectEmitter.event; + } + + get onDidSocketDisconnect(): Event { + return this.onDidSocketDisconnectEmitter.event; + } + + constructor(options: RemoteSessionOptions) { + this.port = options.port; + } + + disconnect(): void { + this.onDidRemoteDisconnectEmitter.fire(); + } + + dispose(): void { + this.onDidSocketDisconnectEmitter.fire(); + } +} diff --git a/packages/remote/src/electron-node/setup/remote-copy-service.ts b/packages/remote/src/electron-node/setup/remote-copy-service.ts new file mode 100644 index 0000000000000..09d9355e3d206 --- /dev/null +++ b/packages/remote/src/electron-node/setup/remote-copy-service.ts @@ -0,0 +1,114 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { ApplicationPackage } from '@theia/core/shared/@theia/application-package'; +import { inject, injectable, named } from '@theia/core/shared/inversify'; +import { RemotePlatform, RemoteCopyContribution, RemoteCopyRegistry, RemoteFile } from '@theia/core/lib/node/remote'; +import { RemoteConnection } from '../remote-types'; +import { RemoteNativeDependencyService } from './remote-native-dependency-service'; +import { ContributionProvider } from '@theia/core'; +import * as archiver from 'archiver'; +import * as path from 'path'; +import * as fs from 'fs'; +import * as os from 'os'; + +@injectable() +export class RemoteCopyService { + + @inject(ApplicationPackage) + protected readonly applicationPackage: ApplicationPackage; + + @inject(RemoteCopyRegistry) + protected readonly copyRegistry: RemoteCopyRegistry; + + @inject(RemoteNativeDependencyService) + protected readonly nativeDependencyService: RemoteNativeDependencyService; + + @inject(ContributionProvider) @named(RemoteCopyContribution) + protected readonly copyContributions: ContributionProvider; + + protected initialized = false; + + async copyToRemote(remote: RemoteConnection, remotePlatform: RemotePlatform, destination: string): Promise { + const zipName = path.basename(destination); + const projectPath = this.applicationPackage.projectPath; + const tempDir = await this.getTempDir(); + const zipPath = path.join(tempDir, zipName); + const files = await this.getFiles(remotePlatform, tempDir); + // We stream to a file here and then copy it because it is faster + // Copying files via sftp is 4x times faster compared to readable streams + const stream = fs.createWriteStream(zipPath); + const archive = archiver('tar', { + gzip: true + }); + archive.pipe(stream); + for (const file of files) { + const filePath = path.isAbsolute(file.path) + ? file.path + : path.join(projectPath, file.path); + + archive.file(filePath, { + name: file.target, + mode: file.options?.mode + }); + } + await archive.finalize(); + await remote.copy(zipPath, destination); + await fs.promises.rm(tempDir, { + recursive: true, + force: true + }); + } + + protected async getFiles(remotePlatform: RemotePlatform, tempDir: string): Promise { + const [localFiles, nativeDependencies] = await Promise.all([ + this.loadCopyContributions(), + this.loadNativeDependencies(remotePlatform, tempDir) + ]); + return [...localFiles, ...nativeDependencies]; + } + + protected async loadCopyContributions(): Promise { + if (this.initialized) { + return this.copyRegistry.getFiles(); + } + await Promise.all(this.copyContributions.getContributions() + .map(copyContribution => copyContribution.copy(this.copyRegistry))); + this.initialized = true; + return this.copyRegistry.getFiles(); + } + + protected async loadNativeDependencies(remotePlatform: RemotePlatform, tempDir: string): Promise { + const dependencyFiles = await this.nativeDependencyService.downloadDependencies(remotePlatform, tempDir); + return dependencyFiles.map(file => ({ + path: file.path, + target: file.target, + options: { + mode: file.mode + } + })); + } + + protected async getTempDir(): Promise { + const dir = path.join(os.tmpdir(), 'theia-remote-'); + const tempDir = await fs.promises.mkdtemp(dir); + return tempDir; + } + + protected async getRemoteDownloadLocation(): Promise { + return undefined; + } +} diff --git a/packages/remote/src/electron-node/setup/remote-native-dependency-service.ts b/packages/remote/src/electron-node/setup/remote-native-dependency-service.ts new file mode 100644 index 0000000000000..4682bec2a9e87 --- /dev/null +++ b/packages/remote/src/electron-node/setup/remote-native-dependency-service.ts @@ -0,0 +1,110 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { ContributionProvider, THEIA_VERSION } from '@theia/core'; +import { inject, injectable, named } from '@theia/core/shared/inversify'; +import { DependencyDownload, DirectoryDependencyDownload, RemoteNativeDependencyContribution, RemotePlatform } from '@theia/core/lib/node/remote'; +import { RequestContext, RequestService, RequestOptions } from '@theia/core/shared/@theia/request'; +import * as decompress from 'decompress'; +import * as path from 'path'; +import * as fs from 'fs/promises'; + +const decompressTar = require('decompress-tar'); +const decompressTargz = require('decompress-targz'); +const decompressUnzip = require('decompress-unzip'); + +export const DEFAULT_HTTP_OPTIONS = { + method: 'GET', + headers: { + Accept: 'application/octet-stream' + }, +}; + +export interface NativeDependencyFile { + path: string; + target: string; + mode?: number; +} + +@injectable() +export class RemoteNativeDependencyService { + + @inject(ContributionProvider) @named(RemoteNativeDependencyContribution) + protected nativeDependencyContributions: ContributionProvider; + + @inject(RequestService) + protected requestService: RequestService; + + async downloadDependencies(remotePlatform: RemotePlatform, directory: string): Promise { + const contributionResults = await Promise.all(this.nativeDependencyContributions.getContributions() + .map(async contribution => { + const result = await contribution.download({ + remotePlatform, + theiaVersion: THEIA_VERSION, + download: requestInfo => this.downloadDependency(requestInfo) + }); + const dependency = await this.storeDependency(result, directory); + return dependency; + })); + return contributionResults.flat(); + } + + protected async downloadDependency(downloadURI: string | RequestOptions): Promise { + const options = typeof downloadURI === 'string' + ? { url: downloadURI, ...DEFAULT_HTTP_OPTIONS } + : { ...DEFAULT_HTTP_OPTIONS, ...downloadURI }; + const req = await this.requestService.request(options); + if (RequestContext.isSuccess(req)) { + return Buffer.from(req.buffer); + } else { + throw new Error('Server error while downloading native dependency from: ' + options.url); + } + } + + protected async storeDependency(dependency: DependencyDownload, directory: string): Promise { + if (DirectoryDependencyDownload.is(dependency)) { + const archiveBuffer = dependency.buffer; + const plugins: unknown[] = []; + if (dependency.archive === 'tar') { + plugins.push(decompressTar()); + } else if (dependency.archive === 'tgz') { + plugins.push(decompressTargz()); + } else if (dependency.archive === 'zip') { + plugins.push(decompressUnzip()); + } + const files = await decompress(archiveBuffer, directory, { plugins }); + const result: NativeDependencyFile[] = await Promise.all(files.map(async file => { + const localPath = path.join(directory, file.path); + return { + path: localPath, + target: file.path, + mode: file.mode + }; + })); + return result; + } else { + const fileName = path.basename(dependency.file.path); + const localPath = path.join(directory, fileName); + await fs.writeFile(localPath, dependency.buffer); + return [{ + path: localPath, + target: dependency.file.path, + mode: dependency.file.mode + }]; + } + } + +} diff --git a/packages/remote/src/electron-node/setup/remote-node-setup-service.ts b/packages/remote/src/electron-node/setup/remote-node-setup-service.ts new file mode 100644 index 0000000000000..892d086ba119d --- /dev/null +++ b/packages/remote/src/electron-node/setup/remote-node-setup-service.ts @@ -0,0 +1,89 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import * as path from 'path'; +import * as fs from '@theia/core/shared/fs-extra'; +import * as os from 'os'; + +import { inject, injectable } from '@theia/core/shared/inversify'; +import { RemotePlatform } from '@theia/core/lib/node/remote'; +import { RequestService } from '@theia/core/shared/@theia/request'; +import { RemoteSetupScriptService } from './remote-setup-script-service'; + +/** + * The current node version that Theia recommends. + * + * Native dependencies are compiled against this version. + */ +export const REMOTE_NODE_VERSION = '18.17.0'; + +@injectable() +export class RemoteNodeSetupService { + + @inject(RequestService) + protected readonly requestService: RequestService; + + @inject(RemoteSetupScriptService) + protected readonly scriptService: RemoteSetupScriptService; + + getNodeDirectoryName(platform: RemotePlatform): string { + const platformId = platform === 'windows' ? 'win' : platform; + // Always use x64 architecture for now + const arch = 'x64'; + const dirName = `node-v${REMOTE_NODE_VERSION}-${platformId}-${arch}`; + return dirName; + } + + getNodeFileName(platform: RemotePlatform): string { + let fileExtension = ''; + if (platform === 'windows') { + fileExtension = 'zip'; + } else if (platform === 'darwin') { + fileExtension = 'tar.gz'; + } else { + fileExtension = 'tar.xz'; + } + return `${this.getNodeDirectoryName(platform)}.${fileExtension}`; + } + + async downloadNode(platform: RemotePlatform): Promise { + const fileName = this.getNodeFileName(platform); + const tmpdir = os.tmpdir(); + const localPath = path.join(tmpdir, fileName); + if (!await fs.pathExists(localPath)) { + const downloadPath = this.getDownloadPath(fileName); + const downloadResult = await this.requestService.request({ + url: downloadPath + }); + await fs.writeFile(localPath, downloadResult.buffer); + } + return localPath; + } + + generateDownloadScript(platform: RemotePlatform, targetPath: string): string { + const fileName = this.getNodeFileName(platform); + const downloadPath = this.getDownloadPath(fileName); + const zipPath = RemotePlatform.joinPath(platform, targetPath, fileName); + const download = this.scriptService.downloadFile(platform, downloadPath, zipPath); + const unzip = this.scriptService.unzip(zipPath, targetPath); + return this.scriptService.joinScript(platform, download, unzip); + } + + protected getDownloadPath(fileName: string): string { + return `https://nodejs.org/dist/v${REMOTE_NODE_VERSION}/${fileName}`; + } + +} diff --git a/packages/remote/src/electron-node/setup/remote-setup-script-service.ts b/packages/remote/src/electron-node/setup/remote-setup-script-service.ts new file mode 100644 index 0000000000000..e3af8de6340a2 --- /dev/null +++ b/packages/remote/src/electron-node/setup/remote-setup-script-service.ts @@ -0,0 +1,57 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { injectable } from '@theia/core/shared/inversify'; +import { RemotePlatform } from '@theia/core/lib/node/remote'; + +@injectable() +export class RemoteSetupScriptService { + + downloadFile(platform: RemotePlatform, url: string, output: string): string { + if (platform === 'windows') { + return `Invoke-WebRequest -Uri "${url}" -OutFile ${output}`; + } else { + return ` +if [ "$(command -v wget)" ]; then + echo "Downloading using wget" + wget -O "${output}" "${url}" +elif [ "$(command -v curl)" ]; then + echo "Downloading using curl" + curl "${url}" --output "${output}" +else + echo "Failed to find wget or curl." + exit 1 +fi +`.trim(); + } + } + + unzip(file: string, directory: string): string { + return `tar -xf "${file}" -C "${directory}"`; + } + + mkdir(platform: RemotePlatform, path: string): string { + if (platform === 'windows') { + return `New-Item -Force -itemType Directory -Path "${path}"`; + } else { + return `mkdir -p "${path}"`; + } + } + + joinScript(platform: RemotePlatform, ...segments: string[]): string { + return segments.join(platform === 'windows' ? '\r\n' : '\n'); + } +} diff --git a/packages/remote/src/electron-node/setup/remote-setup-service.ts b/packages/remote/src/electron-node/setup/remote-setup-service.ts new file mode 100644 index 0000000000000..e3e312ccd3b76 --- /dev/null +++ b/packages/remote/src/electron-node/setup/remote-setup-service.ts @@ -0,0 +1,163 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { inject, injectable } from '@theia/core/shared/inversify'; +import { RemoteConnection, RemoteExecResult, RemoteStatusReport } from '../remote-types'; +import { ApplicationPackage } from '@theia/core/shared/@theia/application-package'; +import { RemoteCopyService } from './remote-copy-service'; +import { RemoteNativeDependencyService } from './remote-native-dependency-service'; +import { THEIA_VERSION } from '@theia/core'; +import { RemoteNodeSetupService } from './remote-node-setup-service'; +import { RemotePlatform } from '@theia/core/lib/node/remote'; +import { RemoteSetupScriptService } from './remote-setup-script-service'; + +@injectable() +export class RemoteSetupService { + + @inject(RemoteCopyService) + protected readonly copyService: RemoteCopyService; + + @inject(RemoteNativeDependencyService) + protected readonly nativeDependencyService: RemoteNativeDependencyService; + + @inject(RemoteNodeSetupService) + protected readonly nodeSetupService: RemoteNodeSetupService; + + @inject(RemoteSetupScriptService) + protected readonly scriptService: RemoteSetupScriptService; + + @inject(ApplicationPackage) + protected readonly applicationPackage: ApplicationPackage; + + async setup(connection: RemoteConnection, report: RemoteStatusReport): Promise { + report('Identifying remote system...'); + // 1. Identify remote platform + const platform = await this.detectRemotePlatform(connection); + // 2. Setup home directory + const remoteHome = await this.getRemoteHomeDirectory(connection, platform); + const applicationDirectory = RemotePlatform.joinPath(platform, remoteHome, `.${this.getRemoteAppName()}`); + await this.mkdirRemote(connection, platform, applicationDirectory); + // 3. Download+copy node for that platform + const nodeFileName = this.nodeSetupService.getNodeFileName(platform); + const nodeDirName = this.nodeSetupService.getNodeDirectoryName(platform); + const remoteNodeDirectory = RemotePlatform.joinPath(platform, applicationDirectory, nodeDirName); + const nodeDirExists = await this.dirExistsRemote(connection, remoteNodeDirectory); + if (!nodeDirExists) { + report('Downloading and installing Node.js on remote...'); + // Download the binaries locally and move it via SSH + const nodeArchive = await this.nodeSetupService.downloadNode(platform); + const remoteNodeZip = RemotePlatform.joinPath(platform, applicationDirectory, nodeFileName); + await connection.copy(nodeArchive, remoteNodeZip); + await this.unzipRemote(connection, remoteNodeZip, applicationDirectory); + } + // 4. Copy backend to remote system + const libDir = RemotePlatform.joinPath(platform, applicationDirectory, 'lib'); + const libDirExists = await this.dirExistsRemote(connection, libDir); + if (!libDirExists) { + report('Installing application on remote...'); + const applicationZipFile = RemotePlatform.joinPath(platform, applicationDirectory, `${this.getRemoteAppName()}.tar`); + await this.copyService.copyToRemote(connection, platform, applicationZipFile); + await this.unzipRemote(connection, applicationZipFile, applicationDirectory); + } + // 5. start remote backend + report('Starting application on remote...'); + const port = await this.startApplication(connection, platform, applicationDirectory, remoteNodeDirectory); + connection.remotePort = port; + } + + protected async startApplication(connection: RemoteConnection, platform: RemotePlatform, remotePath: string, nodeDir: string): Promise { + const nodeExecutable = RemotePlatform.joinPath(platform, nodeDir, 'bin', platform === 'windows' ? 'node.exe' : 'node'); + const mainJsFile = RemotePlatform.joinPath(platform, remotePath, 'lib', 'backend', 'main.js'); + const localAddressRegex = /listening on http:\/\/127.0.0.1:(\d+)/; + // Change to the remote application path and start a node process with the copied main.js file + // This way, our current working directory is set as expected + const result = await connection.execPartial(`cd "${remotePath}";${nodeExecutable}`, + stdout => localAddressRegex.test(stdout), + [mainJsFile, '--hostname=127.0.0.1', '--port=0', '--remote']); + + const match = localAddressRegex.exec(result.stdout); + if (!match) { + throw new Error('Could not start remote system: ' + result.stdout); + } else { + return Number(match[1]); + } + } + + protected async detectRemotePlatform(connection: RemoteConnection): Promise { + const result = await connection.exec('uname -s'); + + if (result.stderr) { + // Only Windows systems return an error output here + return 'windows'; + } else if (result.stdout) { + if (result.stdout.includes('windows32') || result.stdout.includes('MINGW64')) { + return 'windows'; + } else if (result.stdout.includes('Linux')) { + return 'linux'; + } else if (result.stdout.includes('Darwin')) { + return 'darwin'; + } + } + throw new Error('Failed to identify remote system: ' + result.stdout + '\n' + result.stderr); + } + + protected async getRemoteHomeDirectory(connection: RemoteConnection, platform: RemotePlatform): Promise { + if (platform === 'windows') { + const powershellHome = await connection.exec('PowerShell -Command $HOME'); + return powershellHome.stdout.trim(); + } else { + const result = await connection.exec('eval echo ~'); + return result.stdout.trim(); + } + } + + protected getRemoteAppName(): string { + const appName = this.applicationPackage.pck.name || 'theia'; + const appVersion = this.applicationPackage.pck.version || THEIA_VERSION; + return `${this.cleanupDirectoryName(`${appName}-${appVersion}`)}-remote`; + } + + protected cleanupDirectoryName(name: string): string { + return name.replace(/[@<>:"\\|?*]/g, '').replace(/\//g, '-'); + } + + protected async mkdirRemote(connection: RemoteConnection, platform: RemotePlatform, remotePath: string): Promise { + const result = await connection.exec(this.scriptService.mkdir(platform, remotePath)); + if (result.stderr) { + throw new Error('Failed to create directory: ' + result.stderr); + } + } + + protected async dirExistsRemote(connection: RemoteConnection, remotePath: string): Promise { + const cdResult = await connection.exec(`cd "${remotePath}"`); + return !Boolean(cdResult.stderr); + } + + protected async unzipRemote(connection: RemoteConnection, remoteFile: string, remoteDirectory: string): Promise { + const result = await connection.exec(this.scriptService.unzip(remoteFile, remoteDirectory)); + if (result.stderr) { + throw new Error('Failed to unzip: ' + result.stderr); + } + } + + protected async executeScriptRemote(connection: RemoteConnection, platform: RemotePlatform, script: string): Promise { + if (platform === 'windows') { + return connection.exec('PowerShell -Command', [script]); + } else { + return connection.exec('sh -c', [script]); + } + } +} diff --git a/packages/remote/src/electron-node/ssh/remote-ssh-connection-provider.ts b/packages/remote/src/electron-node/ssh/remote-ssh-connection-provider.ts new file mode 100644 index 0000000000000..f0b944ef4867a --- /dev/null +++ b/packages/remote/src/electron-node/ssh/remote-ssh-connection-provider.ts @@ -0,0 +1,355 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import * as fs from '@theia/core/shared/fs-extra'; +import { Emitter, Event, MessageService, QuickInputService } from '@theia/core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { RemoteSSHConnectionProvider } from '../../electron-common/remote-ssh-connection-provider'; +import { RemoteConnectionService } from '../remote-connection-service'; +import { RemoteProxyServerProvider } from '../remote-proxy-server-provider'; +import { RemoteConnection, RemoteExecOptions, RemoteExecResult, RemoteExecTester, RemoteStatusReport } from '../remote-types'; +import * as ssh2 from 'ssh2'; +import SftpClient = require('ssh2-sftp-client'); +import * as net from 'net'; +import { Deferred, timeout } from '@theia/core/lib/common/promise-util'; +import { SSHIdentityFileCollector, SSHKey } from './ssh-identity-file-collector'; +import { RemoteSetupService } from '../setup/remote-setup-service'; + +@injectable() +export class RemoteSSHConnectionProviderImpl implements RemoteSSHConnectionProvider { + + @inject(RemoteConnectionService) + protected readonly remoteConnectionService: RemoteConnectionService; + + @inject(RemoteProxyServerProvider) + protected readonly serverProvider: RemoteProxyServerProvider; + + @inject(SSHIdentityFileCollector) + protected readonly identityFileCollector: SSHIdentityFileCollector; + + @inject(RemoteSetupService) + protected readonly remoteSetup: RemoteSetupService; + + @inject(QuickInputService) + protected readonly quickInputService: QuickInputService; + + @inject(MessageService) + protected readonly messageService: MessageService; + + protected passwordRetryCount = 3; + protected passphraseRetryCount = 3; + + async establishConnection(host: string, user: string): Promise { + const progress = await this.messageService.showProgress({ + text: 'Remote SSH' + }); + const report: RemoteStatusReport = message => progress.report({ message }); + report('Connecting to remote system...'); + try { + const remote = await this.establishSSHConnection(host, user); + await this.remoteSetup.setup(remote, report); + const registration = this.remoteConnectionService.register(remote); + const server = await this.serverProvider.getProxyServer(socket => { + remote.forwardOut(socket); + }); + remote.onDidDisconnect(() => { + server.close(); + registration.dispose(); + }); + const localPort = (server.address() as net.AddressInfo).port; + remote.localPort = localPort; + return localPort.toString(); + } finally { + progress.cancel(); + } + } + + async establishSSHConnection(host: string, user: string): Promise { + const deferred = new Deferred(); + const sessionId = this.remoteConnectionService.getConnectionId(); + const sshClient = new ssh2.Client(); + const identityFiles = await this.identityFileCollector.gatherIdentityFiles(); + const sshAuthHandler = this.getAuthHandler(user, host, identityFiles); + sshClient + .on('ready', async () => { + const connection = new RemoteSSHConnection({ + client: sshClient, + id: sessionId, + name: host, + type: 'SSH' + }); + try { + await this.testConnection(connection); + deferred.resolve(connection); + } catch (err) { + deferred.reject(err); + } + }).on('error', err => { + deferred.reject(err); + }).connect({ + host: host, + username: user, + authHandler: (methodsLeft, successes, callback) => (sshAuthHandler(methodsLeft, successes, callback), undefined) + }); + return deferred.promise; + } + + /** + * Sometimes, ssh2.exec will not execute and retrieve any data right after the `ready` event fired. + * In this method, we just perform `echo hello` in a loop to ensure that the connection is really ready. + * See also https://github.com/mscdex/ssh2/issues/48 + */ + protected async testConnection(connection: RemoteSSHConnection): Promise { + for (let i = 0; i < 100; i++) { + const result = await connection.exec('echo hello'); + if (result.stdout.includes('hello')) { + return; + } + await timeout(50); + } + throw new Error('SSH connection failed testing. Could not execute "echo"'); + } + + protected getAuthHandler(user: string, host: string, identityKeys: SSHKey[]): ssh2.AuthHandlerMiddleware { + let passwordRetryCount = this.passwordRetryCount; + let keyboardRetryCount = this.passphraseRetryCount; + // `false` is a valid return value, indicating that the authentication has failed + const END_AUTH = false as unknown as ssh2.AuthenticationType; + // `null` indicates that we just want to continue with the next auth type + // eslint-disable-next-line no-null/no-null + const NEXT_AUTH = null as unknown as ssh2.AuthenticationType; + return async (methodsLeft: string[] | null, _partialSuccess: boolean | null, callback: ssh2.NextAuthHandler) => { + if (!methodsLeft) { + return callback({ + type: 'none', + username: user, + }); + } + if (methodsLeft && methodsLeft.includes('publickey') && identityKeys.length) { + const identityKey = identityKeys.shift()!; + if (identityKey.isPrivate) { + return callback({ + type: 'publickey', + username: user, + key: identityKey.parsedKey + }); + } + if (!await fs.pathExists(identityKey.filename)) { + // Try next identity file + return callback(NEXT_AUTH); + } + + const keyBuffer = await fs.promises.readFile(identityKey.filename); + let result = ssh2.utils.parseKey(keyBuffer); // First try without passphrase + if (result instanceof Error && result.message === 'Encrypted private OpenSSH key detected, but no passphrase given') { + let passphraseRetryCount = this.passphraseRetryCount; + while (result instanceof Error && passphraseRetryCount > 0) { + const passphrase = await this.quickInputService.input({ + title: `Enter passphrase for ${identityKey.filename}`, + password: true + }); + if (!passphrase) { + break; + } + result = ssh2.utils.parseKey(keyBuffer, passphrase); + passphraseRetryCount--; + } + } + if (!result || result instanceof Error) { + // Try next identity file + return callback(NEXT_AUTH); + } + + const key = Array.isArray(result) ? result[0] : result; + return callback({ + type: 'publickey', + username: user, + key + }); + } + if (methodsLeft && methodsLeft.includes('password') && passwordRetryCount > 0) { + const password = await this.quickInputService.input({ + title: `Enter password for ${user}@${host}`, + password: true + }); + passwordRetryCount--; + + return callback(password + ? { + type: 'password', + username: user, + password + } + : END_AUTH); + } + if (methodsLeft && methodsLeft.includes('keyboard-interactive') && keyboardRetryCount > 0) { + return callback({ + type: 'keyboard-interactive', + username: user, + prompt: async (_name, _instructions, _instructionsLang, prompts, finish) => { + const responses: string[] = []; + for (const prompt of prompts) { + const response = await this.quickInputService.input({ + title: `(${user}@${host}) ${prompt.prompt}`, + password: !prompt.echo + }); + if (response === undefined) { + keyboardRetryCount = 0; + break; + } + responses.push(response); + } + keyboardRetryCount--; + finish(responses); + } + }); + } + + callback(END_AUTH); + }; + } + + isConnectionAlive(remoteId: string): Promise { + return Promise.resolve(Boolean(this.remoteConnectionService.getConnection(remoteId))); + } + +} + +export interface RemoteSSHConnectionOptions { + id: string; + name: string; + type: string; + client: ssh2.Client; +} + +export class RemoteSSHConnection implements RemoteConnection { + + id: string; + name: string; + type: string; + client: ssh2.Client; + localPort = 0; + remotePort = 0; + + private sftpClientPromise: Promise; + + private readonly onDidDisconnectEmitter = new Emitter(); + + get onDidDisconnect(): Event { + return this.onDidDisconnectEmitter.event; + } + + constructor(options: RemoteSSHConnectionOptions) { + this.id = options.id; + this.type = options.type; + this.name = options.name; + this.client = options.client; + this.onDidDisconnect(() => this.dispose()); + this.client.on('end', () => { + this.onDidDisconnectEmitter.fire(); + }); + this.sftpClientPromise = this.setupSftpClient(); + } + + protected async setupSftpClient(): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const sftpClient = new SftpClient() as any; + // A hack to set the internal ssh2 client of the sftp client + // That way, we don't have to create a second connection + sftpClient.client = this.client; + // Calling this function establishes the sftp connection on the ssh client + await sftpClient.getSftpChannel(); + return sftpClient; + } + + forwardOut(socket: net.Socket): void { + this.client.forwardOut(socket.localAddress!, socket.localPort!, '127.0.0.1', this.remotePort, (err, stream) => { + if (err) { + console.debug('Proxy message rejected', err); + } else { + stream.pipe(socket).pipe(stream); + } + }); + } + + async copy(localPath: string, remotePath: string): Promise { + const sftpClient = await this.sftpClientPromise; + await sftpClient.put(localPath, remotePath); + } + + exec(cmd: string, args?: string[], options: RemoteExecOptions = {}): Promise { + const deferred = new Deferred(); + cmd = this.buildCmd(cmd, args); + this.client.exec(cmd, options, (err, stream) => { + if (err) { + return deferred.reject(err); + } + let stdout = ''; + let stderr = ''; + stream.on('close', () => { + deferred.resolve({ stdout, stderr }); + }).on('data', (data: Buffer | string) => { + stdout += data.toString(); + }).stderr.on('data', (data: Buffer | string) => { + stderr += data.toString(); + }); + }); + return deferred.promise; + } + + execPartial(cmd: string, tester: RemoteExecTester, args?: string[], options: RemoteExecOptions = {}): Promise { + const deferred = new Deferred(); + cmd = this.buildCmd(cmd, args); + this.client.exec(cmd, { + ...options, + // Ensure that the process on the remote ends when the connection is closed + pty: true + }, (err, stream) => { + if (err) { + return deferred.reject(err); + } + // in pty mode we only have an stdout stream + // return stdout as stderr as well + let stdout = ''; + stream.on('close', () => { + if (deferred.state === 'unresolved') { + deferred.resolve({ stdout, stderr: stdout }); + } + }).on('data', (data: Buffer | string) => { + if (deferred.state === 'unresolved') { + stdout += data.toString(); + + if (tester(stdout, stdout)) { + deferred.resolve({ stdout, stderr: stdout }); + } + } + }); + }); + return deferred.promise; + } + + protected buildCmd(cmd: string, args?: string[]): string { + const escapedArgs = args?.map(arg => `"${arg.replace(/"/g, '\\"')}"`) || []; + const fullCmd = cmd + (escapedArgs.length > 0 ? (' ' + escapedArgs.join(' ')) : ''); + return fullCmd; + } + + dispose(): void { + this.client.end(); + this.client.destroy(); + } + +} diff --git a/packages/remote/src/electron-node/ssh/ssh-identity-file-collector.ts b/packages/remote/src/electron-node/ssh/ssh-identity-file-collector.ts new file mode 100644 index 0000000000000..f4f6581585455 --- /dev/null +++ b/packages/remote/src/electron-node/ssh/ssh-identity-file-collector.ts @@ -0,0 +1,137 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import * as fs from '@theia/core/shared/fs-extra'; +import * as os from 'os'; +import * as path from 'path'; +import * as crypto from 'crypto'; +import { ParsedKey } from 'ssh2'; +import * as ssh2 from 'ssh2'; +import { injectable } from '@theia/core/shared/inversify'; + +export interface SSHKey { + filename: string; + parsedKey: ParsedKey; + fingerprint: string; + agentSupport?: boolean; + isPrivate?: boolean; +} + +@injectable() +export class SSHIdentityFileCollector { + + protected getDefaultIdentityFiles(): string[] { + const homeDir = os.homedir(); + const PATH_SSH_CLIENT_ID_DSA = path.join(homeDir, '.ssh', '/id_dsa'); + const PATH_SSH_CLIENT_ID_ECDSA = path.join(homeDir, '.ssh', '/id_ecdsa'); + const PATH_SSH_CLIENT_ID_RSA = path.join(homeDir, '.ssh', '/id_rsa'); + const PATH_SSH_CLIENT_ID_ED25519 = path.join(homeDir, '.ssh', '/id_ed25519'); + const PATH_SSH_CLIENT_ID_XMSS = path.join(homeDir, '.ssh', '/id_xmss'); + const PATH_SSH_CLIENT_ID_ECDSA_SK = path.join(homeDir, '.ssh', '/id_ecdsa_sk'); + const PATH_SSH_CLIENT_ID_ED25519_SK = path.join(homeDir, '.ssh', '/id_ed25519_sk'); + + return [ + PATH_SSH_CLIENT_ID_DSA, + PATH_SSH_CLIENT_ID_ECDSA, + PATH_SSH_CLIENT_ID_ECDSA_SK, + PATH_SSH_CLIENT_ID_ED25519, + PATH_SSH_CLIENT_ID_ED25519_SK, + PATH_SSH_CLIENT_ID_RSA, + PATH_SSH_CLIENT_ID_XMSS + ]; + } + + async gatherIdentityFiles(sshAgentSock?: string): Promise { + const identityFiles = this.getDefaultIdentityFiles(); + + const identityFileContentsResult = await Promise.allSettled(identityFiles.map(async keyPath => { + keyPath = await fs.pathExists(keyPath + '.pub') ? keyPath + '.pub' : keyPath; + return fs.promises.readFile(keyPath); + })); + const fileKeys: SSHKey[] = identityFileContentsResult.map((result, i) => { + if (result.status === 'rejected') { + return undefined; + } + + const parsedResult = ssh2.utils.parseKey(result.value); + if (parsedResult instanceof Error || !parsedResult) { + console.log(`Error while parsing SSH public key ${identityFiles[i]}:`, parsedResult); + return undefined; + } + + const parsedKey = Array.isArray(parsedResult) ? parsedResult[0] : parsedResult; + const fingerprint = crypto.createHash('sha256').update(parsedKey.getPublicSSH()).digest('base64'); + + return { + filename: identityFiles[i], + parsedKey, + fingerprint + }; + }).filter((v: T | undefined): v is T => !!v); + + let sshAgentParsedKeys: ParsedKey[] = []; + if (sshAgentSock) { + sshAgentParsedKeys = await new Promise((resolve, reject) => { + const sshAgent = new ssh2.OpenSSHAgent(sshAgentSock); + sshAgent.getIdentities((err, publicKeys) => { + if (err) { + reject(err); + } else if (publicKeys) { + resolve(publicKeys.map(key => { + if ('pubKey' in key) { + const pubKey = key.pubKey; + if ('pubKey' in pubKey) { + return pubKey.pubKey as ParsedKey; + } + return pubKey; + } else { + return key; + } + })); + } else { + resolve([]); + } + }); + }); + } + + const sshAgentKeys: SSHKey[] = sshAgentParsedKeys.map(parsedKey => { + const fingerprint = crypto.createHash('sha256').update(parsedKey.getPublicSSH()).digest('base64'); + return { + filename: parsedKey.comment, + parsedKey, + fingerprint, + agentSupport: true + }; + }); + + const agentKeys: SSHKey[] = []; + const preferredIdentityKeys: SSHKey[] = []; + for (const agentKey of sshAgentKeys) { + const foundIdx = fileKeys.findIndex(k => agentKey.parsedKey.type === k.parsedKey.type && agentKey.fingerprint === k.fingerprint); + if (foundIdx >= 0) { + preferredIdentityKeys.push({ ...fileKeys[foundIdx], agentSupport: true }); + fileKeys.splice(foundIdx, 1); + } else { + agentKeys.push(agentKey); + } + } + preferredIdentityKeys.push(...agentKeys); + preferredIdentityKeys.push(...fileKeys); + + return preferredIdentityKeys; + } +} diff --git a/packages/remote/src/package.spec.ts b/packages/remote/src/package.spec.ts new file mode 100644 index 0000000000000..6268b6a14d9b6 --- /dev/null +++ b/packages/remote/src/package.spec.ts @@ -0,0 +1,29 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +/* note: this bogus test file is required so that + we are able to run mocha unit tests on this + package, without having any actual unit tests in it. + This way a coverage report will be generated, + showing 0% coverage, instead of no report. + This file can be removed once we have real unit + tests in place. */ + +describe('remote package', () => { + + it('support code coverage statistics', () => true); + +}); diff --git a/packages/remote/tsconfig.json b/packages/remote/tsconfig.json new file mode 100644 index 0000000000000..b623c1e105ac7 --- /dev/null +++ b/packages/remote/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "composite": true, + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../core" + } + ] +} diff --git a/scripts/zip-native-dependencies.js b/scripts/zip-native-dependencies.js new file mode 100644 index 0000000000000..f1ecfee3aa01e --- /dev/null +++ b/scripts/zip-native-dependencies.js @@ -0,0 +1,53 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +const { promisify } = require('util'); +const glob = promisify(require('glob')); +const fs = require('fs'); +const path = require('path'); +const archiver = require('archiver'); + +async function run() { + const repoPath = path.resolve(__dirname, '..'); + const zipFile = path.join(__dirname, `native-dependencies-${process.platform}-${process.arch}.zip`); + const browserAppPath = path.join(repoPath, 'examples', 'browser'); + const nativeDependencies = await glob('lib/backend/native/**', { + cwd: browserAppPath + }); + const buildDependencies = await glob('lib/build/Release/**', { + cwd: browserAppPath + }); + const trashDependencies = await glob('lib/backend/{windows-trash.exe,macos-trash}', { + cwd: browserAppPath + }); + const archive = archiver('zip'); + const output = fs.createWriteStream(zipFile, { flags: "w" }); + archive.pipe(output); + for (const file of [ + ...nativeDependencies, + ...buildDependencies, + ...trashDependencies + ]) { + const filePath = path.join(browserAppPath, file); + archive.file(filePath, { + name: file, + mode: (await fs.promises.stat(filePath)).mode + }); + } + await archive.finalize(); +} + +run(); diff --git a/tsconfig.json b/tsconfig.json index 8677247fe9d0d..732d0029c6500 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -144,6 +144,9 @@ { "path": "packages/property-view" }, + { + "path": "packages/remote" + }, { "path": "packages/scm" }, diff --git a/yarn.lock b/yarn.lock index 56e297965e736..4b16bb5b92105 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1695,6 +1695,13 @@ "@tufjs/canonical-json" "1.0.0" minimatch "^9.0.0" +"@types/archiver@^5.3.2": + version "5.3.3" + resolved "https://registry.yarnpkg.com/@types/archiver/-/archiver-5.3.3.tgz#9cb632a67060602b1658c669b498d51dd8ce08ab" + integrity sha512-0ABdVcXL6jOwNGY+hjWPqrxUvKelBEwNLcuv/SV2vZ4YCH8w9NttFCt+/QqI5zgMX+iX/XqVy89/r7EmLJmMpQ== + dependencies: + "@types/readdir-glob" "*" + "@types/bent@^7.0.1": version "7.3.3" resolved "https://registry.yarnpkg.com/@types/bent/-/bent-7.3.3.tgz#b8daa06e72219045b3f67f968d590d3df3875d96" @@ -1775,6 +1782,13 @@ dependencies: "@types/node" "*" +"@types/decompress@^4.2.4": + version "4.2.5" + resolved "https://registry.yarnpkg.com/@types/decompress/-/decompress-4.2.5.tgz#07ed5b350303b945017692e87a653a09df166915" + integrity sha512-LdL+kbcKGs9TzvB/K+OBGzPfDoP6gwwTsykYjodlzUJUUYp/43c1p1jE5YTtz3z4Ml90iruvBXbJ6+kDvb3WSQ== + dependencies: + "@types/node" "*" + "@types/diff@^3.2.2": version "3.5.5" resolved "https://registry.yarnpkg.com/@types/diff/-/diff-3.5.5.tgz#d1ddc082c03a26f0490856da47d57c29093d1e76" @@ -1813,6 +1827,13 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194" integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA== +"@types/express-http-proxy@^1.6.3": + version "1.6.4" + resolved "https://registry.yarnpkg.com/@types/express-http-proxy/-/express-http-proxy-1.6.4.tgz#42917facb194ab476c06f381838f211f4717a7dc" + integrity sha512-V0THpGPqxR85uHARStjYSKObI7ett4qA1JtiRqv/rv7pAt8IYFCtieLeq0GPnVYeR1BghgGQYlEZK7JPMUPrDQ== + dependencies: + "@types/express" "*" + "@types/express-serve-static-core@^4.17.33": version "4.17.35" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz#c95dd4424f0d32e525d23812aa8ab8e4d3906c4f" @@ -1847,7 +1868,7 @@ dependencies: "@types/node" "*" -"@types/glob@*": +"@types/glob@*", "@types/glob@^8.1.0": version "8.1.0" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-8.1.0.tgz#b63e70155391b0584dce44e7ea25190bbc38f2fc" integrity sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w== @@ -2024,7 +2045,7 @@ "@types/node" "*" form-data "^3.0.0" -"@types/node@*", "@types/node@18", "@types/node@>=10.0.0", "@types/node@^10.14.22", "@types/node@^16.11.26": +"@types/node@*", "@types/node@18", "@types/node@>=10.0.0", "@types/node@^10.14.22", "@types/node@^16.11.26", "@types/node@^18.11.18": version "18.16.19" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.19.tgz#cb03fca8910fdeb7595b755126a8a78144714eea" integrity sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA== @@ -2082,6 +2103,13 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/readdir-glob@*": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@types/readdir-glob/-/readdir-glob-1.1.2.tgz#688a206aa258a3a5d17c7b053da3b9e04eabf431" + integrity sha512-vwAYrNN/8yhp/FJRU6HUSD0yk6xfoOS8HrZa8ZL7j+X8hJpaC1hTcAiXX2IxaAkkvrz9mLyoEhYZTE3cEYvA9Q== + dependencies: + "@types/node" "*" + "@types/responselike@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" @@ -2148,6 +2176,20 @@ resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz#bf2e02a3dbd4aecaf95942ecd99b7402e03fad5e" integrity sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA== +"@types/ssh2-sftp-client@^9.0.0": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@types/ssh2-sftp-client/-/ssh2-sftp-client-9.0.1.tgz#653e6088d34a1f7ffa32253d7fbcf853a318109e" + integrity sha512-jz3I1vFxUezHNOl5Bppj1AiltsVh3exudiLJI3ImOz80pSWMDb+aCT5qBHSWfQyJd5QOUEV7/+jSewIVNvwzrg== + dependencies: + "@types/ssh2" "*" + +"@types/ssh2@*", "@types/ssh2@^1.11.11": + version "1.11.14" + resolved "https://registry.yarnpkg.com/@types/ssh2/-/ssh2-1.11.14.tgz#d3bebf27fa508add5ddb65d0c945ca5329e669fb" + integrity sha512-O/U38mvV4jVVrdtZz8KpmitkmeD/PUDeDNNueQhm34166dmaqb1iZ3sfarSxBArM2/iX4PZVJY3EOta0Zks9hw== + dependencies: + "@types/node" "^18.11.18" + "@types/tar-fs@^1.16.1": version "1.16.3" resolved "https://registry.yarnpkg.com/@types/tar-fs/-/tar-fs-1.16.3.tgz#425b2b817c405d13d051f36ec6ec6ebd25e31069" @@ -2843,6 +2885,51 @@ aproba@^1.0.3: resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== +archiver-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-2.1.0.tgz#e8a460e94b693c3e3da182a098ca6285ba9249e2" + integrity sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw== + dependencies: + glob "^7.1.4" + graceful-fs "^4.2.0" + lazystream "^1.0.0" + lodash.defaults "^4.2.0" + lodash.difference "^4.5.0" + lodash.flatten "^4.4.0" + lodash.isplainobject "^4.0.6" + lodash.union "^4.6.0" + normalize-path "^3.0.0" + readable-stream "^2.0.0" + +archiver-utils@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-3.0.4.tgz#a0d201f1cf8fce7af3b5a05aea0a337329e96ec7" + integrity sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw== + dependencies: + glob "^7.2.3" + graceful-fs "^4.2.0" + lazystream "^1.0.0" + lodash.defaults "^4.2.0" + lodash.difference "^4.5.0" + lodash.flatten "^4.4.0" + lodash.isplainobject "^4.0.6" + lodash.union "^4.6.0" + normalize-path "^3.0.0" + readable-stream "^3.6.0" + +archiver@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/archiver/-/archiver-5.3.2.tgz#99991d5957e53bd0303a392979276ac4ddccf3b0" + integrity sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw== + dependencies: + archiver-utils "^2.1.0" + async "^3.2.4" + buffer-crc32 "^0.2.1" + readable-stream "^3.6.0" + readdir-glob "^1.1.2" + tar-stream "^2.2.0" + zip-stream "^4.1.0" + archy@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" @@ -3009,7 +3096,7 @@ async-mutex@^0.4.0: dependencies: tslib "^2.4.0" -async@^3.2.3: +async@^3.2.3, async@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== @@ -3311,7 +3398,7 @@ buffer-alloc@^1.2.0: buffer-alloc-unsafe "^1.1.0" buffer-fill "^1.0.0" -buffer-crc32@~0.2.3: +buffer-crc32@^0.2.1, buffer-crc32@^0.2.13, buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== @@ -3803,6 +3890,16 @@ compare-func@^2.0.0: array-ify "^1.0.0" dot-prop "^5.1.0" +compress-commons@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-4.1.2.tgz#6542e59cb63e1f46a8b21b0e06f9a32e4c8b06df" + integrity sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg== + dependencies: + buffer-crc32 "^0.2.13" + crc32-stream "^4.0.2" + normalize-path "^3.0.0" + readable-stream "^3.6.0" + compression-webpack-plugin@^9.0.0: version "9.2.0" resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-9.2.0.tgz#57fd539d17c5907eebdeb4e83dcfe2d7eceb9ef6" @@ -4055,6 +4152,19 @@ cpu-features@~0.0.8: buildcheck "~0.0.6" nan "^2.17.0" +crc-32@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" + integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== + +crc32-stream@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-4.0.3.tgz#85dd677eb78fa7cad1ba17cc506a597d41fc6f33" + integrity sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw== + dependencies: + crc-32 "^1.2.0" + readable-stream "^3.4.0" + cross-fetch@3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" @@ -4178,7 +4288,7 @@ debug@4.3.3: dependencies: ms "2.1.2" -debug@^3.1.0, debug@^3.2.7: +debug@^3.0.1, debug@^3.1.0, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== @@ -4821,7 +4931,7 @@ es6-error@^4.0.1, es6-error@^4.1.1: resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== -es6-promise@^4.2.4: +es6-promise@^4.1.1, es6-promise@^4.2.4: version "4.2.8" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== @@ -5156,6 +5266,15 @@ exponential-backoff@^3.1.1: resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw== +express-http-proxy@^1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/express-http-proxy/-/express-http-proxy-1.6.3.tgz#f3ef139ffd49a7962e7af0462bbcca557c913175" + integrity sha512-/l77JHcOUrDUX8V67E287VEUQT0lbm71gdGVoodnlWBziarYKgMcpqT7xvh/HM8Jv52phw8Bd8tY+a7QjOr7Yg== + dependencies: + debug "^3.0.1" + es6-promise "^4.1.1" + raw-body "^2.3.0" + express@^4.16.3: version "4.18.2" resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" @@ -5825,7 +5944,7 @@ glob@^10.2.2: minipass "^5.0.0 || ^6.0.2 || ^7.0.0" path-scurry "^1.10.1" -glob@^7.0.6, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7, glob@^7.2.0: +glob@^7.0.6, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7, glob@^7.2.0, glob@^7.2.3: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -5837,7 +5956,7 @@ glob@^7.0.6, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, gl once "^1.3.0" path-is-absolute "^1.0.0" -glob@^8.0.1, glob@^8.0.3: +glob@^8.0.1, glob@^8.0.3, glob@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== @@ -6959,6 +7078,13 @@ kind-of@^6.0.2, kind-of@^6.0.3: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== +lazystream@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638" + integrity sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw== + dependencies: + readable-stream "^2.0.5" + lerna@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/lerna/-/lerna-7.1.1.tgz#6703062e6c4ddefdaf41e8890e9200690924fd71" @@ -7209,6 +7335,21 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ== + +lodash.difference@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c" + integrity sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA== + +lodash.flatten@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" + integrity sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g== + lodash.flattendeep@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" @@ -7224,6 +7365,11 @@ lodash.ismatch@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" integrity sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g== +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -7239,6 +7385,11 @@ lodash.truncate@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== +lodash.union@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" + integrity sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw== + lodash@^4.17.15, lodash@^4.17.21, lodash@^4.5.1: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -7883,6 +8034,11 @@ nanoid@3.3.3: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== +nanoid@3.3.4: + version "3.3.4" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" + integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== + nanoid@^3.3.6: version "3.3.6" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" @@ -9227,7 +9383,7 @@ raw-body@2.5.1: iconv-lite "0.4.24" unpipe "1.0.0" -raw-body@2.5.2: +raw-body@2.5.2, raw-body@^2.3.0: version "2.5.2" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== @@ -9378,7 +9534,7 @@ read@^2.0.0: dependencies: mute-stream "~1.0.0" -readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.5, readable-stream@~2.3.6: +readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.5, readable-stream@~2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -9400,6 +9556,13 @@ readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.1.1, readable string_decoder "^1.1.1" util-deprecate "^1.0.1" +readdir-glob@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.3.tgz#c3d831f51f5e7bfa62fa2ffbe4b508c640f09584" + integrity sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA== + dependencies: + minimatch "^5.1.0" + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -10191,7 +10354,16 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== -ssh2@^1.5.0: +ssh2-sftp-client@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/ssh2-sftp-client/-/ssh2-sftp-client-9.1.0.tgz#bef1d5352c3dc214836cb6dec9427f3988d9ff2b" + integrity sha512-Hzdr9OE6GxZjcmyM9tgBSIFVyrHAp9c6U2Y4yBkmYOHoQvZ7pIm27dmltvcmRfxcWiIcg8HBvG5iAikDf+ZuzQ== + dependencies: + concat-stream "^2.0.0" + promise-retry "^2.0.1" + ssh2 "^1.12.0" + +ssh2@^1.12.0, ssh2@^1.5.0: version "1.14.0" resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-1.14.0.tgz#8f68440e1b768b66942c9e4e4620b2725b3555bb" integrity sha512-AqzD1UCqit8tbOKoj6ztDDi1ffJZ2rV2SwlgrVVrHPkV5vWqGJOVp5pmtj18PunkPJAuKQsnInyKV+/Nb2bUnA== @@ -10516,7 +10688,7 @@ tar-stream@^1.1.2, tar-stream@^1.5.2: to-buffer "^1.1.1" xtend "^4.0.0" -tar-stream@^2.1.4, tar-stream@~2.2.0: +tar-stream@^2.1.4, tar-stream@^2.2.0, tar-stream@~2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== @@ -11714,3 +11886,12 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zip-stream@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-4.1.1.tgz#1337fe974dbaffd2fa9a1ba09662a66932bd7135" + integrity sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ== + dependencies: + archiver-utils "^3.0.4" + compress-commons "^4.1.2" + readable-stream "^3.6.0" From 98ba11fa5ea1c33e062a07b220aea0927aad2c89 Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Mon, 23 Oct 2023 14:26:52 +0200 Subject: [PATCH 02/12] Review comments --- .github/workflows/native-dependencies.yml | 5 +- packages/core/package.json | 5 - .../core/src/browser/credentials-service.ts | 6 +- .../browser/frontend-application-module.ts | 10 +- packages/core/src/browser/index.ts | 1 - .../messaging/ws-connection-provider.ts | 38 +++--- .../{keytar-protocol.ts => key-store.ts} | 6 +- .../hosting/electron-ws-origin-validator.ts | 2 +- .../token/electron-token-validator.ts | 2 + .../src/node/backend-application-module.ts | 10 +- .../{remote => }/backend-remote-service.ts | 0 .../{keytar-server.ts => key-store-server.ts} | 42 +++---- .../node/messaging/messaging-contribution.ts | 4 +- .../src/node/remote/backend-remote-module.ts | 35 ------ packages/core/src/node/remote/index.ts | 20 --- .../electron-file-dialog-service.ts | 8 -- .../src/main/node/plugin-service.ts | 7 +- packages/remote/package.json | 4 +- .../remote-electron-file-dialog-service.ts | 47 +++++++ .../remote-frontend-contribution.ts | 10 +- .../remote-frontend-module.ts | 11 +- .../electron-browser/remote-service-impl.ts | 28 ----- .../src/electron-browser}/remote-service.ts | 17 +-- .../remote-ssh-contribution.ts | 26 ++-- .../backend-remote-service-impl.ts | 2 +- .../electron-node/remote-backend-module.ts | 32 +++-- .../remote-connection-service.ts | 5 - .../electron-node/remote-tunnel-service.ts | 56 --------- .../remote/src/electron-node/remote-types.ts | 39 +----- .../app-native-dependency-contribution.ts | 9 +- .../setup/main-copy-contribution.ts} | 4 +- .../setup}/remote-copy-contribution.ts | 11 +- .../setup/remote-copy-service.ts | 12 +- .../remote-native-dependency-contribution.ts | 16 +-- .../setup/remote-native-dependency-service.ts | 3 +- .../setup/remote-node-setup-service.ts | 15 ++- .../setup/remote-setup-script-service.ts | 119 +++++++++++++++--- .../setup/remote-setup-service.ts | 92 ++++++++------ .../ssh/remote-ssh-connection-provider.ts | 21 +++- 39 files changed, 386 insertions(+), 394 deletions(-) rename packages/core/src/common/{keytar-protocol.ts => key-store.ts} (89%) rename packages/core/src/node/{remote => }/backend-remote-service.ts (100%) rename packages/core/src/node/{keytar-server.ts => key-store-server.ts} (81%) delete mode 100644 packages/core/src/node/remote/backend-remote-module.ts delete mode 100644 packages/core/src/node/remote/index.ts create mode 100644 packages/remote/src/electron-browser/remote-electron-file-dialog-service.ts delete mode 100644 packages/remote/src/electron-browser/remote-service-impl.ts rename packages/{core/src/browser => remote/src/electron-browser}/remote-service.ts (78%) delete mode 100644 packages/remote/src/electron-node/remote-tunnel-service.ts rename packages/{core/src/node/remote => remote/src/electron-node/setup}/app-native-dependency-contribution.ts (82%) rename packages/{core/src/node/remote/core-copy-contribution.ts => remote/src/electron-node/setup/main-copy-contribution.ts} (90%) rename packages/{core/src/node/remote => remote/src/electron-node/setup}/remote-copy-contribution.ts (88%) rename packages/{core/src/node/remote => remote/src/electron-node/setup}/remote-native-dependency-contribution.ts (83%) diff --git a/.github/workflows/native-dependencies.yml b/.github/workflows/native-dependencies.yml index 10096bc61314c..095ff9912e9fc 100644 --- a/.github/workflows/native-dependencies.yml +++ b/.github/workflows/native-dependencies.yml @@ -12,16 +12,17 @@ jobs: - name: Checkout uses: actions/checkout@v3 + # Update the node version here after every Electron upgrade - name: Use Node.js 18.17.0 uses: actions/setup-node@v3 with: node-version: '18.17.0' registry-url: 'https://registry.npmjs.org' - - name: Use Python 3.x + - name: Use Python 3.11 uses: actions/setup-python@v4 with: - python-version: '3.x' + python-version: '3.11' - name: Install and Build shell: bash diff --git a/packages/core/package.json b/packages/core/package.json index 513e0bce42bf1..b11b4c87047f3 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -23,7 +23,6 @@ "@types/dompurify": "^2.2.2", "@types/express": "^4.16.0", "@types/fs-extra": "^4.0.2", - "@types/glob": "^8.1.0", "@types/lodash.debounce": "4.0.3", "@types/lodash.throttle": "^4.1.3", "@types/markdown-it": "^12.2.3", @@ -47,7 +46,6 @@ "font-awesome": "^4.7.0", "fs-extra": "^4.0.2", "fuzzy": "^0.1.3", - "glob": "^8.1.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "iconv-lite": "^0.6.0", @@ -168,9 +166,6 @@ { "backend": "lib/node/request/backend-request-module", "backendElectron": "lib/electron-node/request/electron-backend-request-module" - }, - { - "backend": "lib/node/remote/backend-remote-module" } ], "keywords": [ diff --git a/packages/core/src/browser/credentials-service.ts b/packages/core/src/browser/credentials-service.ts index dbe7793aed9c7..66ae2dd9035cd 100644 --- a/packages/core/src/browser/credentials-service.ts +++ b/packages/core/src/browser/credentials-service.ts @@ -22,7 +22,7 @@ import { inject, injectable } from 'inversify'; import { Emitter, Event } from '../common/event'; -import { KeytarService } from '../common/keytar-protocol'; +import { KeyStoreService } from '../common/key-store'; export interface CredentialsProvider { getPassword(service: string, account: string): Promise; @@ -50,7 +50,7 @@ export class CredentialsServiceImpl implements CredentialsService { private credentialsProvider: CredentialsProvider; - constructor(@inject(KeytarService) private readonly keytarService: KeytarService) { + constructor(@inject(KeyStoreService) private readonly keytarService: KeyStoreService) { this.credentialsProvider = new KeytarCredentialsProvider(this.keytarService); } @@ -82,7 +82,7 @@ export class CredentialsServiceImpl implements CredentialsService { class KeytarCredentialsProvider implements CredentialsProvider { - constructor(private readonly keytarService: KeytarService) { } + constructor(private readonly keytarService: KeyStoreService) { } deletePassword(service: string, account: string): Promise { return this.keytarService.deletePassword(service, account); diff --git a/packages/core/src/browser/frontend-application-module.ts b/packages/core/src/browser/frontend-application-module.ts index 13b7b254f6917..87121d5c91a10 100644 --- a/packages/core/src/browser/frontend-application-module.ts +++ b/packages/core/src/browser/frontend-application-module.ts @@ -97,7 +97,7 @@ import { EncodingRegistry } from './encoding-registry'; import { EncodingService } from '../common/encoding-service'; import { AuthenticationService, AuthenticationServiceImpl } from '../browser/authentication-service'; import { DecorationsService, DecorationsServiceImpl } from './decorations-service'; -import { keytarServicePath, KeytarService } from '../common/keytar-protocol'; +import { keyStoreServicePath, KeyStoreService } from '../common/key-store'; import { CredentialsService, CredentialsServiceImpl } from './credentials-service'; import { ContributionFilterRegistry, ContributionFilterRegistryImpl } from '../common/contribution-filter'; import { QuickCommandFrontendContribution } from './quick-input/quick-command-frontend-contribution'; @@ -137,7 +137,6 @@ import { MarkdownRenderer, MarkdownRendererFactory, MarkdownRendererImpl } from import { StylingParticipant, StylingService } from './styling-service'; import { bindCommonStylingParticipants } from './common-styling-participants'; import { HoverService } from './hover-service'; -import { NullRemoteService, RemoteService } from './remote-service'; import { AdditionalViewsMenuWidget, AdditionalViewsMenuWidgetFactory } from './shell/additional-views-menu-widget'; export { bindResourceProvider, bindMessageService, bindPreferenceService }; @@ -400,9 +399,9 @@ export const frontendApplicationModule = new ContainerModule((bind, _unbind, _is bind(AuthenticationService).to(AuthenticationServiceImpl).inSingletonScope(); bind(DecorationsService).to(DecorationsServiceImpl).inSingletonScope(); - bind(KeytarService).toDynamicValue(ctx => { + bind(KeyStoreService).toDynamicValue(ctx => { const connection = ctx.container.get(WebSocketConnectionProvider); - return connection.createProxy(keytarServicePath); + return connection.createProxy(keyStoreServicePath); }).inSingletonScope(); bind(CredentialsService).to(CredentialsServiceImpl); @@ -450,8 +449,5 @@ export const frontendApplicationModule = new ContainerModule((bind, _unbind, _is bindContributionProvider(bind, StylingParticipant); bind(FrontendApplicationContribution).toService(StylingService); - bind(NullRemoteService).toSelf().inSingletonScope(); - bind(RemoteService).toService(NullRemoteService); - bind(SecondaryWindowHandler).toSelf().inSingletonScope(); }); diff --git a/packages/core/src/browser/index.ts b/packages/core/src/browser/index.ts index 889859bfa8dbd..eecaa565401dc 100644 --- a/packages/core/src/browser/index.ts +++ b/packages/core/src/browser/index.ts @@ -45,4 +45,3 @@ export * from './tooltip-service'; export * from './decoration-style'; export * from './styling-service'; export * from './hover-service'; -export * from './remote-service'; diff --git a/packages/core/src/browser/messaging/ws-connection-provider.ts b/packages/core/src/browser/messaging/ws-connection-provider.ts index d91bf2d518568..05e8a25de9928 100644 --- a/packages/core/src/browser/messaging/ws-connection-provider.ts +++ b/packages/core/src/browser/messaging/ws-connection-provider.ts @@ -14,7 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { injectable, interfaces, decorate, unmanaged, postConstruct } from 'inversify'; +import { injectable, interfaces, decorate, unmanaged } from 'inversify'; import { RpcProxyFactory, RpcProxy, Emitter, Event, Channel } from '../../common'; import { Endpoint } from '../endpoint'; import { AbstractConnectionProvider } from '../../common/messaging/abstract-connection-provider'; @@ -63,15 +63,23 @@ export class WebSocketConnectionProvider extends AbstractConnectionProvider { + this.initializeMultiplexer(); + if (this.reconnectChannelOpeners.length > 0) { + this.reconnectChannelOpeners.forEach(opener => opener()); + this.reconnectChannelOpeners = []; + } + this.socket.on('disconnect', () => this.fireSocketDidClose()); + this.socket.on('message', () => this.onIncomingMessageActivityEmitter.fire(undefined)); + this.fireSocketDidOpen(); + }); + this.socket.connect(); } protected createMainChannel(): Channel { @@ -122,22 +130,6 @@ export class WebSocketConnectionProvider extends AbstractConnectionProvider { - this.initializeMultiplexer(); - if (this.reconnectChannelOpeners.length > 0) { - this.reconnectChannelOpeners.forEach(opener => opener()); - this.reconnectChannelOpeners = []; - } - this.socket.on('disconnect', () => this.fireSocketDidClose()); - this.socket.on('message', () => this.onIncomingMessageActivityEmitter.fire(undefined)); - this.fireSocketDidOpen(); - }); - this.socket.connect(); - } - /** * Creates a web socket for the given url */ diff --git a/packages/core/src/common/keytar-protocol.ts b/packages/core/src/common/key-store.ts similarity index 89% rename from packages/core/src/common/keytar-protocol.ts rename to packages/core/src/common/key-store.ts index 9b491d55b0b81..68de0cf8ddd18 100644 --- a/packages/core/src/common/keytar-protocol.ts +++ b/packages/core/src/common/key-store.ts @@ -14,10 +14,10 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -export const keytarServicePath = '/services/keytar'; +export const keyStoreServicePath = '/services/keyStore'; -export const KeytarService = Symbol('KeytarService'); -export interface KeytarService { +export const KeyStoreService = Symbol('KeyStoreService'); +export interface KeyStoreService { setPassword(service: string, account: string, password: string): Promise; getPassword(service: string, account: string): Promise; deletePassword(service: string, account: string): Promise; diff --git a/packages/core/src/electron-node/hosting/electron-ws-origin-validator.ts b/packages/core/src/electron-node/hosting/electron-ws-origin-validator.ts index d9bceab8db793..43494b9ffd4c5 100644 --- a/packages/core/src/electron-node/hosting/electron-ws-origin-validator.ts +++ b/packages/core/src/electron-node/hosting/electron-ws-origin-validator.ts @@ -16,7 +16,7 @@ import * as http from 'http'; import { inject, injectable } from 'inversify'; -import { BackendRemoteService } from '../../node/remote/backend-remote-service'; +import { BackendRemoteService } from '../../node/backend-remote-service'; import { WsRequestValidatorContribution } from '../../node/ws-request-validators'; @injectable() diff --git a/packages/core/src/electron-node/token/electron-token-validator.ts b/packages/core/src/electron-node/token/electron-token-validator.ts index cc8da02a8c478..8572b1ed9e18d 100644 --- a/packages/core/src/electron-node/token/electron-token-validator.ts +++ b/packages/core/src/electron-node/token/electron-token-validator.ts @@ -84,6 +84,8 @@ export class ElectronTokenValidator implements WsRequestValidatorContribution { if (token) { return JSON.parse(token); } else { + // No token has been passed to the backend server + // That indicates we're running without a local frontend return undefined; } } diff --git a/packages/core/src/node/backend-application-module.ts b/packages/core/src/node/backend-application-module.ts index d414491e83c5b..e3461c473b956 100644 --- a/packages/core/src/node/backend-application-module.ts +++ b/packages/core/src/node/backend-application-module.ts @@ -31,8 +31,8 @@ import { EnvVariablesServerImpl } from './env-variables'; import { ConnectionContainerModule } from './messaging/connection-container-module'; import { QuickInputService, quickInputServicePath, QuickPickService, quickPickServicePath } from '../common/quick-pick-service'; import { WsRequestValidator, WsRequestValidatorContribution } from './ws-request-validators'; -import { KeytarService, keytarServicePath } from '../common/keytar-protocol'; -import { KeytarServiceImpl } from './keytar-server'; +import { KeyStoreService, keyStoreServicePath } from '../common/key-store'; +import { KeyStoreServiceImpl } from './key-store-server'; import { ContributionFilterRegistry, ContributionFilterRegistryImpl } from '../common/contribution-filter'; import { EnvironmentUtils } from './environment-utils'; import { ProcessUtils } from './process-utils'; @@ -41,6 +41,7 @@ import { bindNodeStopwatch, bindBackendStopwatchServer } from './performance'; import { OSBackendProviderImpl } from './os-backend-provider'; import { BackendRequestFacade } from './request/backend-request-facade'; import { FileSystemLocking, FileSystemLockingImpl } from './filesystem-locking'; +import { BackendRemoteService } from './backend-remote-service'; decorate(injectable(), ApplicationPackage); @@ -107,9 +108,9 @@ export const backendApplicationModule = new ContainerModule(bind => { bind(WsRequestValidator).toSelf().inSingletonScope(); bindContributionProvider(bind, WsRequestValidatorContribution); - bind(KeytarService).to(KeytarServiceImpl).inSingletonScope(); + bind(KeyStoreService).to(KeyStoreServiceImpl).inSingletonScope(); bind(ConnectionHandler).toDynamicValue(ctx => - new RpcConnectionHandler(keytarServicePath, () => ctx.container.get(KeytarService)) + new RpcConnectionHandler(keyStoreServicePath, () => ctx.container.get(KeyStoreService)) ).inSingletonScope(); bind(ContributionFilterRegistry).to(ContributionFilterRegistryImpl).inSingletonScope(); @@ -126,6 +127,7 @@ export const backendApplicationModule = new ContainerModule(bind => { bind(ProxyCliContribution).toSelf().inSingletonScope(); bind(CliContribution).toService(ProxyCliContribution); + bind(BackendRemoteService).toSelf().inSingletonScope(); bind(BackendRequestFacade).toSelf().inSingletonScope(); bind(ConnectionHandler).toDynamicValue( ctx => new RpcConnectionHandler(REQUEST_SERVICE_PATH, () => ctx.container.get(BackendRequestFacade)) diff --git a/packages/core/src/node/remote/backend-remote-service.ts b/packages/core/src/node/backend-remote-service.ts similarity index 100% rename from packages/core/src/node/remote/backend-remote-service.ts rename to packages/core/src/node/backend-remote-service.ts diff --git a/packages/core/src/node/keytar-server.ts b/packages/core/src/node/key-store-server.ts similarity index 81% rename from packages/core/src/node/keytar-server.ts rename to packages/core/src/node/key-store-server.ts index e1bbf034981f8..0e6d0490e6175 100644 --- a/packages/core/src/node/keytar-server.ts +++ b/packages/core/src/node/key-store-server.ts @@ -20,31 +20,27 @@ *--------------------------------------------------------------------------------------------*/ // code copied and modified from https://github.com/microsoft/vscode/blob/1.55.2/src/vs/platform/native/electron-main/nativeHostMainService.ts#L679-L771 -import { KeytarService } from '../common/keytar-protocol'; -import { inject, injectable } from 'inversify'; +import { KeyStoreService } from '../common/key-store'; +import { injectable } from 'inversify'; import { isWindows } from '../common'; -import { BackendRemoteService } from './remote/backend-remote-service'; @injectable() -export class KeytarServiceImpl implements KeytarService { - - @inject(BackendRemoteService) - protected readonly backendRemoteService: BackendRemoteService; +export class KeyStoreServiceImpl implements KeyStoreService { private static readonly MAX_PASSWORD_LENGTH = 2500; - private static readonly PASSWORD_CHUNK_SIZE = KeytarServiceImpl.MAX_PASSWORD_LENGTH - 100; + private static readonly PASSWORD_CHUNK_SIZE = KeyStoreServiceImpl.MAX_PASSWORD_LENGTH - 100; - protected cache?: typeof import('keytar'); + protected keytarImplementation?: typeof import('keytar'); async setPassword(service: string, account: string, password: string): Promise { const keytar = await this.getKeytar(); - if (isWindows && password.length > KeytarServiceImpl.MAX_PASSWORD_LENGTH) { + if (isWindows && password.length > KeyStoreServiceImpl.MAX_PASSWORD_LENGTH) { let index = 0; let chunk = 0; let hasNextChunk = true; while (hasNextChunk) { - const passwordChunk = password.substring(index, index + KeytarServiceImpl.PASSWORD_CHUNK_SIZE); - index += KeytarServiceImpl.PASSWORD_CHUNK_SIZE; + const passwordChunk = password.substring(index, index + KeyStoreServiceImpl.PASSWORD_CHUNK_SIZE); + index += KeyStoreServiceImpl.PASSWORD_CHUNK_SIZE; hasNextChunk = password.length - index > 0; const content: ChunkedPassword = { @@ -94,9 +90,7 @@ export class KeytarServiceImpl implements KeytarService { async findPassword(service: string): Promise { const keytar = await this.getKeytar(); const password = await keytar.findPassword(service); - if (password) { - return password; - } + return password ?? undefined; } async findCredentials(service: string): Promise> { @@ -105,20 +99,18 @@ export class KeytarServiceImpl implements KeytarService { } protected async getKeytar(): Promise { - if (this.cache) { - return this.cache; + if (this.keytarImplementation) { + return this.keytarImplementation; } try { - return (this.cache = await import('keytar')); + this.keytarImplementation = await import('keytar'); + // Try using keytar to see if it throws or not. + await this.keytarImplementation.findCredentials('test-keytar-loads'); } catch (err) { - if (this.backendRemoteService.isRemoteServer()) { - // When running as a remote server, the necessary prerequisites might not be installed on the system - // Just use an in-memory cache for credentials - return (this.cache = new InMemoryCredentialsProvider()); - } else { - throw err; - } + this.keytarImplementation = new InMemoryCredentialsProvider(); + console.warn('OS level credential store could not be accessed. Using in-memory credentials provider', err); } + return this.keytarImplementation; } } diff --git a/packages/core/src/node/messaging/messaging-contribution.ts b/packages/core/src/node/messaging/messaging-contribution.ts index a6de18c22d315..2e628a1795762 100644 --- a/packages/core/src/node/messaging/messaging-contribution.ts +++ b/packages/core/src/node/messaging/messaging-contribution.ts @@ -60,11 +60,11 @@ export class MessagingContribution implements BackendApplicationContribution, Me } wsChannel(spec: string, callback: (params: MessagingService.PathParams, channel: Channel) => void): void { - return this.channelHandlers.push(spec, (params, channel) => callback(params, channel)); + this.channelHandlers.push(spec, (params, channel) => callback(params, channel)); } ws(spec: string, callback: (params: MessagingService.PathParams, socket: Socket) => void): void { - return this.wsHandlers.push(spec, callback); + this.wsHandlers.push(spec, callback); } protected checkAliveTimeout = 30000; // 30 seconds diff --git a/packages/core/src/node/remote/backend-remote-module.ts b/packages/core/src/node/remote/backend-remote-module.ts deleted file mode 100644 index 099f12c57a771..0000000000000 --- a/packages/core/src/node/remote/backend-remote-module.ts +++ /dev/null @@ -1,35 +0,0 @@ -// ***************************************************************************** -// Copyright (C) 2023 TypeFox and others. -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License v. 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0. -// -// This Source Code may also be made available under the following Secondary -// Licenses when the conditions for such availability set forth in the Eclipse -// Public License v. 2.0 are satisfied: GNU General Public License, version 2 -// with the GNU Classpath Exception which is available at -// https://www.gnu.org/software/classpath/license.html. -// -// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 -// ***************************************************************************** - -import { ContainerModule } from 'inversify'; -import { bindContributionProvider } from '../../common'; -import { CoreCopyContribution } from './core-copy-contribution'; -import { RemoteCopyContribution, RemoteCopyRegistry } from './remote-copy-contribution'; -import { RemoteNativeDependencyContribution } from './remote-native-dependency-contribution'; -import { AppNativeDependencyContribution } from './app-native-dependency-contribution'; -import { BackendRemoteService } from './backend-remote-service'; - -export default new ContainerModule(bind => { - bind(BackendRemoteService).toSelf().inSingletonScope(); - bindContributionProvider(bind, RemoteCopyContribution); - bind(RemoteCopyRegistry).toSelf().inSingletonScope(); - bind(CoreCopyContribution).toSelf().inSingletonScope(); - bind(RemoteCopyContribution).toService(CoreCopyContribution); - - bindContributionProvider(bind, RemoteNativeDependencyContribution); - bind(AppNativeDependencyContribution).toSelf().inSingletonScope(); - bind(RemoteNativeDependencyContribution).toService(AppNativeDependencyContribution); -}); diff --git a/packages/core/src/node/remote/index.ts b/packages/core/src/node/remote/index.ts deleted file mode 100644 index c8dd5ed896c43..0000000000000 --- a/packages/core/src/node/remote/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -// ***************************************************************************** -// Copyright (C) 2023 TypeFox and others. -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License v. 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0. -// -// This Source Code may also be made available under the following Secondary -// Licenses when the conditions for such availability set forth in the Eclipse -// Public License v. 2.0 are satisfied: GNU General Public License, version 2 -// with the GNU Classpath Exception which is available at -// https://www.gnu.org/software/classpath/license.html. -// -// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 -// ***************************************************************************** - -export * from './core-copy-contribution'; -export * from './remote-copy-contribution'; -export * from './remote-native-dependency-contribution'; -export * from './app-native-dependency-contribution'; diff --git a/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts b/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts index 829a83f6b2f9b..abb9c57372cb8 100644 --- a/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts +++ b/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts @@ -16,7 +16,6 @@ import { inject, injectable } from '@theia/core/shared/inversify'; import URI from '@theia/core/lib/common/uri'; -import { RemoteService } from '@theia/core/lib/browser/remote-service'; import { MaybeArray } from '@theia/core/lib/common/types'; import { MessageService } from '@theia/core/lib/common/message-service'; import { FileStat } from '../../common/files'; @@ -36,14 +35,10 @@ import { OpenDialogOptions, SaveDialogOptions } from '../../electron-common/elec export class ElectronFileDialogService extends DefaultFileDialogService { @inject(MessageService) protected readonly messageService: MessageService; - @inject(RemoteService) protected readonly remoteService: RemoteService; override async showOpenDialog(props: OpenFileDialogProps & { canSelectMany: true }, folder?: FileStat): Promise | undefined>; override async showOpenDialog(props: OpenFileDialogProps, folder?: FileStat): Promise; override async showOpenDialog(props: OpenFileDialogProps, folder?: FileStat): Promise | undefined> { - if (this.remoteService.isConnected()) { - return super.showOpenDialog(props, folder); - } const rootNode = await this.getRootNode(folder); if (rootNode) { const filePaths = await window.electronTheiaFilesystem.showOpenDialog(this.toOpenDialogOptions(rootNode.uri, props)); @@ -60,9 +55,6 @@ export class ElectronFileDialogService extends DefaultFileDialogService { } override async showSaveDialog(props: SaveFileDialogProps, folder?: FileStat): Promise { - if (this.remoteService.isConnected()) { - return super.showSaveDialog(props, folder); - } const rootNode = await this.getRootNode(folder); if (rootNode) { const filePath = await window.electronTheiaFilesystem.showSaveDialog(this.toSaveDialogOptions(rootNode.uri, props)); diff --git a/packages/plugin-ext/src/main/node/plugin-service.ts b/packages/plugin-ext/src/main/node/plugin-service.ts index a01504c335860..91763379a5e0b 100644 --- a/packages/plugin-ext/src/main/node/plugin-service.ts +++ b/packages/plugin-ext/src/main/node/plugin-service.ts @@ -26,7 +26,7 @@ import { environment } from '@theia/core/shared/@theia/application-package/lib/e import { WsRequestValidatorContribution } from '@theia/core/lib/node/ws-request-validators'; import { MaybePromise } from '@theia/core/lib/common'; import { ApplicationPackage } from '@theia/core/shared/@theia/application-package'; -import { BackendRemoteService } from '@theia/core/lib/node/remote/backend-remote-service'; +import { BackendRemoteService } from '@theia/core/lib/node/backend-remote-service'; @injectable() export class PluginApiContribution implements BackendApplicationContribution, WsRequestValidatorContribution { @@ -52,8 +52,9 @@ export class PluginApiContribution implements BackendApplicationContribution, Ws const webviewApp = express(); webviewApp.use('/webview', express.static(path.join(this.applicationPackage.projectPath, 'lib', 'webview', 'pre'))); if (this.remoteService.isRemoteServer()) { - // If we are a remote server, the subdomain information gets lost - // We simply serve the webviews on a path + // Any request to `subdomain.localhost:port/webview/...` will get redirected to the remote system. + // However, it will get redirected directly to the `localhost:remotePort` address, losing the subdomain info. + // In this case, we simply serve the webviews on a path. app.use(webviewApp); } else { app.use(vhost(this.webviewExternalEndpointRegExp, webviewApp)); diff --git a/packages/remote/package.json b/packages/remote/package.json index b841e6a1dd188..c1d12c891ffdd 100644 --- a/packages/remote/package.json +++ b/packages/remote/package.json @@ -4,13 +4,14 @@ "description": "Theia - Remote", "dependencies": { "@theia/core": "1.42.0", + "@theia/filesystem": "1.42.0", "archiver": "^5.3.1", "decompress": "^4.2.1", "decompress-tar": "^4.0.0", "decompress-targz": "^4.0.0", "decompress-unzip": "^4.0.1", "express-http-proxy": "^1.6.3", - "nanoid": "3.3.4", + "glob": "^8.1.0", "ssh2": "^1.12.0", "ssh2-sftp-client": "^9.1.0", "socket.io": "^4.5.3", @@ -55,6 +56,7 @@ "@types/archiver": "^5.3.2", "@types/decompress": "^4.2.4", "@types/express-http-proxy": "^1.6.3", + "@types/glob": "^8.1.0", "@types/ssh2": "^1.11.11", "@types/ssh2-sftp-client": "^9.0.0" }, diff --git a/packages/remote/src/electron-browser/remote-electron-file-dialog-service.ts b/packages/remote/src/electron-browser/remote-electron-file-dialog-service.ts new file mode 100644 index 0000000000000..7ab21738f50ac --- /dev/null +++ b/packages/remote/src/electron-browser/remote-electron-file-dialog-service.ts @@ -0,0 +1,47 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { MaybeArray, URI } from '@theia/core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { OpenFileDialogProps, SaveFileDialogProps } from '@theia/filesystem/lib/browser/file-dialog'; +import { FileStat } from '@theia/filesystem/lib/common/files'; +import { DefaultFileDialogService } from '@theia/filesystem/lib/browser/file-dialog/file-dialog-service'; +import { ElectronFileDialogService } from '@theia/filesystem/lib/electron-browser/file-dialog/electron-file-dialog-service'; +import { RemoteService } from './remote-service'; + +@injectable() +export class RemoteElectronFileDialogService extends ElectronFileDialogService { + + @inject(RemoteService) protected readonly remoteService: RemoteService; + + override showOpenDialog(props: OpenFileDialogProps & { canSelectMany: true; }, folder?: FileStat | undefined): Promise | undefined>; + override showOpenDialog(props: OpenFileDialogProps, folder?: FileStat | undefined): Promise; + override showOpenDialog(props: OpenFileDialogProps, folder?: FileStat): Promise | undefined> | Promise { + if (this.remoteService.isConnected()) { + return DefaultFileDialogService.prototype.showOpenDialog.call(this, props, folder); + } else { + return super.showOpenDialog(props, folder); + } + } + + override showSaveDialog(props: SaveFileDialogProps, folder?: FileStat | undefined): Promise { + if (this.remoteService.isConnected()) { + return DefaultFileDialogService.prototype.showSaveDialog.call(this, props, folder); + } else { + return super.showSaveDialog(props, folder); + } + } +} diff --git a/packages/remote/src/electron-browser/remote-frontend-contribution.ts b/packages/remote/src/electron-browser/remote-frontend-contribution.ts index 40734b356e20b..9db5ed2a9f199 100644 --- a/packages/remote/src/electron-browser/remote-frontend-contribution.ts +++ b/packages/remote/src/electron-browser/remote-frontend-contribution.ts @@ -19,7 +19,7 @@ import { FrontendApplicationContribution, StatusBar, StatusBarAlignment, StatusB import { inject, injectable, named, optional } from '@theia/core/shared/inversify'; import { RemoteStatus, RemoteStatusService } from '../electron-common/remote-status-service'; import { RemoteRegistry, RemoteRegistryContribution } from './remote-registry-contribution'; -import { RemoteServiceImpl } from './remote-service-impl'; +import { RemoteService } from './remote-service'; import { WindowService } from '@theia/core/lib/browser/window/window-service'; export namespace RemoteCommands { @@ -44,8 +44,8 @@ export class RemoteFrontendContribution implements CommandContribution, Frontend @inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry; - @inject(RemoteServiceImpl) - protected readonly remoteService: RemoteServiceImpl; + @inject(RemoteService) + protected readonly remoteService: RemoteService; @inject(RemoteStatusService) protected readonly remoteStatusService: RemoteStatusService; @@ -71,7 +71,7 @@ export class RemoteFrontendContribution implements CommandContribution, Frontend } protected async setStatusBar(info: RemoteStatus): Promise { - this.remoteService.connected = info.alive; + this.remoteService.setConnected(info.alive); const entry: StatusBarEntry = { alignment: StatusBarAlignment.LEFT, command: RemoteCommands.REMOTE_SELECT.id, @@ -87,7 +87,7 @@ export class RemoteFrontendContribution implements CommandContribution, Frontend tooltip: nls.localizeByDefault('Open a Remote Window'), }) }; - this.statusBar.setElement('remoteInfo', entry); + this.statusBar.setElement('remoteStatus', entry); } registerCommands(commands: CommandRegistry): void { diff --git a/packages/remote/src/electron-browser/remote-frontend-module.ts b/packages/remote/src/electron-browser/remote-frontend-module.ts index ceae6edd6ee45..91269cb796b15 100644 --- a/packages/remote/src/electron-browser/remote-frontend-module.ts +++ b/packages/remote/src/electron-browser/remote-frontend-module.ts @@ -16,13 +16,15 @@ import { bindContributionProvider, CommandContribution } from '@theia/core'; import { ContainerModule } from '@theia/core/shared/inversify'; -import { FrontendApplicationContribution, RemoteService, WebSocketConnectionProvider } from '@theia/core/lib/browser'; +import { FrontendApplicationContribution, WebSocketConnectionProvider } from '@theia/core/lib/browser'; import { RemoteSSHContribution } from './remote-ssh-contribution'; import { RemoteSSHConnectionProvider, RemoteSSHConnectionProviderPath } from '../electron-common/remote-ssh-connection-provider'; import { RemoteFrontendContribution } from './remote-frontend-contribution'; import { RemoteRegistryContribution } from './remote-registry-contribution'; -import { RemoteServiceImpl } from './remote-service-impl'; +import { RemoteService } from './remote-service'; import { RemoteStatusService, RemoteStatusServicePath } from '../electron-common/remote-status-service'; +import { ElectronFileDialogService } from '@theia/filesystem/lib/electron-browser/file-dialog/electron-file-dialog-service'; +import { RemoteElectronFileDialogService } from './remote-electron-file-dialog-service'; export default new ContainerModule((bind, _, __, rebind) => { bind(RemoteFrontendContribution).toSelf().inSingletonScope(); @@ -33,8 +35,9 @@ export default new ContainerModule((bind, _, __, rebind) => { bind(RemoteSSHContribution).toSelf().inSingletonScope(); bind(RemoteRegistryContribution).toService(RemoteSSHContribution); - bind(RemoteServiceImpl).toSelf().inSingletonScope(); - rebind(RemoteService).toService(RemoteServiceImpl); + rebind(ElectronFileDialogService).to(RemoteElectronFileDialogService).inSingletonScope(); + + bind(RemoteService).toSelf().inSingletonScope(); bind(RemoteSSHConnectionProvider).toDynamicValue(ctx => WebSocketConnectionProvider.createLocalProxy(ctx.container, RemoteSSHConnectionProviderPath)).inSingletonScope(); diff --git a/packages/remote/src/electron-browser/remote-service-impl.ts b/packages/remote/src/electron-browser/remote-service-impl.ts deleted file mode 100644 index 67689456e968a..0000000000000 --- a/packages/remote/src/electron-browser/remote-service-impl.ts +++ /dev/null @@ -1,28 +0,0 @@ -// ***************************************************************************** -// Copyright (C) 2023 TypeFox and others. -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License v. 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0. -// -// This Source Code may also be made available under the following Secondary -// Licenses when the conditions for such availability set forth in the Eclipse -// Public License v. 2.0 are satisfied: GNU General Public License, version 2 -// with the GNU Classpath Exception which is available at -// https://www.gnu.org/software/classpath/license.html. -// -// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 -// ***************************************************************************** - -import { RemoteService } from '@theia/core/lib/browser'; -import { injectable } from '@theia/core/shared/inversify'; - -@injectable() -export class RemoteServiceImpl implements RemoteService { - - connected: boolean; - - isConnected(): boolean { - return this.connected; - } -} diff --git a/packages/core/src/browser/remote-service.ts b/packages/remote/src/electron-browser/remote-service.ts similarity index 78% rename from packages/core/src/browser/remote-service.ts rename to packages/remote/src/electron-browser/remote-service.ts index e515c66b692c8..5b0ee8c81beff 100644 --- a/packages/core/src/browser/remote-service.ts +++ b/packages/remote/src/electron-browser/remote-service.ts @@ -14,17 +14,18 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { injectable } from 'inversify'; +import { injectable } from '@theia/core/shared/inversify'; -export const RemoteService = Symbol('RemoteService'); +@injectable() +export class RemoteService { -export interface RemoteService { - isConnected(): boolean; -} + protected _connected: boolean; -@injectable() -export class NullRemoteService implements RemoteService { isConnected(): boolean { - return false; + return this._connected; + } + + setConnected(value: boolean): void { + this._connected = value; } } diff --git a/packages/remote/src/electron-browser/remote-ssh-contribution.ts b/packages/remote/src/electron-browser/remote-ssh-contribution.ts index 4367fd0ea919c..23fe5f6369e67 100644 --- a/packages/remote/src/electron-browser/remote-ssh-contribution.ts +++ b/packages/remote/src/electron-browser/remote-ssh-contribution.ts @@ -56,14 +56,28 @@ export class RemoteSSHContribution extends AbstractRemoteRegistryContribution { async connect(newWindow: boolean): Promise { let host: string | undefined; let user: string | undefined; - host = await this.requestQuickInput('host'); - if (host?.includes('@')) { + host = await this.quickInputService.input({ + title: nls.localize('theia/remote/enterHost', 'Enter SSH host name'), + placeHolder: nls.localize('theia/remote/hostPlaceHolder', 'E.g. hello@example.com') + }); + if (!host) { + this.messageService.error(nls.localize('theia/remote/needsHost', 'Please enter a host name.')); + return; + } + if (host.includes('@')) { const split = host.split('@'); user = split[0]; host = split[1]; } if (!user) { - user = await this.requestQuickInput('user'); + user = await this.quickInputService.input({ + title: nls.localize('theia/remote/enterUser', 'Enter SSH user name'), + placeHolder: nls.localize('theia/remote/userPlaceHolder', 'E.g. hello') + }); + } + if (!user) { + this.messageService.error(nls.localize('theia/remote/needsUser', 'Please enter a user name.')); + return; } try { @@ -74,12 +88,6 @@ export class RemoteSSHContribution extends AbstractRemoteRegistryContribution { } } - async requestQuickInput(prompt: string): Promise { - return this.quickInputService.input({ - prompt - }); - } - async sendSSHConnect(host: string, user: string): Promise { return this.sshConnectionProvider.establishConnection(host, user); } diff --git a/packages/remote/src/electron-node/backend-remote-service-impl.ts b/packages/remote/src/electron-node/backend-remote-service-impl.ts index db88035eeb4ef..805481b9eb790 100644 --- a/packages/remote/src/electron-node/backend-remote-service-impl.ts +++ b/packages/remote/src/electron-node/backend-remote-service-impl.ts @@ -17,7 +17,7 @@ import { CliContribution } from '@theia/core/lib/node'; import { injectable } from '@theia/core/shared/inversify'; import { Arguments, Argv } from '@theia/core/shared/yargs'; -import { BackendRemoteService } from '@theia/core/lib/node/remote/backend-remote-service'; +import { BackendRemoteService } from '@theia/core/lib/node/backend-remote-service'; export const REMOTE_START = 'remote'; diff --git a/packages/remote/src/electron-node/remote-backend-module.ts b/packages/remote/src/electron-node/remote-backend-module.ts index f332b14383d96..733929479ce44 100644 --- a/packages/remote/src/electron-node/remote-backend-module.ts +++ b/packages/remote/src/electron-node/remote-backend-module.ts @@ -19,7 +19,6 @@ import { BackendApplicationContribution, CliContribution } from '@theia/core/lib import { RemoteConnectionService } from './remote-connection-service'; import { RemoteProxyServerProvider } from './remote-proxy-server-provider'; import { RemoteConnectionSocketProvider } from './remote-connection-socket-provider'; -import { RemoteTunnelService } from './remote-tunnel-service'; import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connection-container-module'; import { RemoteSSHConnectionProvider, RemoteSSHConnectionProviderPath } from '../electron-common/remote-ssh-connection-provider'; import { RemoteSSHConnectionProviderImpl } from './ssh/remote-ssh-connection-provider'; @@ -28,12 +27,16 @@ import { RemoteCopyService } from './setup/remote-copy-service'; import { RemoteSetupService } from './setup/remote-setup-service'; import { RemoteNativeDependencyService } from './setup/remote-native-dependency-service'; import { BackendRemoteServiceImpl } from './backend-remote-service-impl'; -import { BackendRemoteService } from '@theia/core/lib/node/remote/backend-remote-service'; +import { BackendRemoteService } from '@theia/core/lib/node/backend-remote-service'; import { RemoteNodeSetupService } from './setup/remote-node-setup-service'; -import { RemoteSetupScriptService } from './setup/remote-setup-script-service'; +import { RemotePosixScriptStrategy, RemoteSetupScriptService, RemoteWindowsScriptStrategy } from './setup/remote-setup-script-service'; import { RemoteStatusService, RemoteStatusServicePath } from '../electron-common/remote-status-service'; import { RemoteStatusServiceImpl } from './remote-status-service'; -import { ConnectionHandler, RpcConnectionHandler } from '@theia/core'; +import { ConnectionHandler, RpcConnectionHandler, bindContributionProvider } from '@theia/core'; +import { RemoteCopyContribution, RemoteCopyRegistry } from './setup/remote-copy-contribution'; +import { MainCopyContribution } from './setup/main-copy-contribution'; +import { RemoteNativeDependencyContribution } from './setup/remote-native-dependency-contribution'; +import { AppNativeDependencyContribution } from './setup/app-native-dependency-contribution'; export const remoteConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => { bind(RemoteSSHConnectionProviderImpl).toSelf().inSingletonScope(); @@ -42,14 +45,8 @@ export const remoteConnectionModule = ConnectionContainerModule.create(({ bind, }); export default new ContainerModule((bind, _unbind, _isBound, rebind) => { - bind(RemoteCopyService).toSelf().inSingletonScope(); - bind(RemoteSetupService).toSelf().inSingletonScope(); - bind(RemoteNodeSetupService).toSelf().inSingletonScope(); - bind(RemoteSetupScriptService).toSelf().inSingletonScope(); - bind(RemoteNativeDependencyService).toSelf().inSingletonScope(); bind(RemoteProxyServerProvider).toSelf().inSingletonScope(); bind(RemoteConnectionSocketProvider).toSelf().inSingletonScope(); - bind(RemoteTunnelService).toSelf().inSingletonScope(); bind(RemoteConnectionService).toSelf().inSingletonScope(); bind(BackendApplicationContribution).toService(RemoteConnectionService); bind(RemoteStatusServiceImpl).toSelf().inSingletonScope(); @@ -58,6 +55,21 @@ export default new ContainerModule((bind, _unbind, _isBound, rebind) => { ctx => new RpcConnectionHandler(RemoteStatusServicePath, () => ctx.container.get(RemoteStatusService)) ).inSingletonScope(); + bind(RemoteCopyService).toSelf().inSingletonScope(); + bind(RemoteSetupService).toSelf().inSingletonScope(); + bind(RemoteNodeSetupService).toSelf().inSingletonScope(); + bind(RemoteWindowsScriptStrategy).toSelf().inSingletonScope(); + bind(RemotePosixScriptStrategy).toSelf().inSingletonScope(); + bind(RemoteSetupScriptService).toSelf().inSingletonScope(); + bind(RemoteNativeDependencyService).toSelf().inSingletonScope(); + bind(RemoteCopyRegistry).toSelf().inSingletonScope(); + bindContributionProvider(bind, RemoteCopyContribution); + bindContributionProvider(bind, RemoteNativeDependencyContribution); + bind(MainCopyContribution).toSelf().inSingletonScope(); + bind(RemoteCopyContribution).toService(MainCopyContribution); + bind(AppNativeDependencyContribution).toSelf().inSingletonScope(); + bind(RemoteNativeDependencyContribution).toService(AppNativeDependencyContribution); + bind(ConnectionContainerModule).toConstantValue(remoteConnectionModule); bind(BackendRemoteServiceImpl).toSelf().inSingletonScope(); diff --git a/packages/remote/src/electron-node/remote-connection-service.ts b/packages/remote/src/electron-node/remote-connection-service.ts index 9e30189f693c3..ce776b8590fc4 100644 --- a/packages/remote/src/electron-node/remote-connection-service.ts +++ b/packages/remote/src/electron-node/remote-connection-service.ts @@ -16,7 +16,6 @@ import { inject, injectable } from '@theia/core/shared/inversify'; import { RemoteConnection } from './remote-types'; -import { nanoid } from 'nanoid'; import { Disposable } from '@theia/core'; import { RemoteCopyService } from './setup/remote-copy-service'; import { RemoteNativeDependencyService } from './setup/remote-native-dependency-service'; @@ -41,10 +40,6 @@ export class RemoteConnectionService implements BackendApplicationContribution { return Array.from(this.connections.values()).find(connection => connection.localPort === port); } - getConnectionId(): string { - return nanoid(10); - } - register(connection: RemoteConnection): Disposable { this.connections.set(connection.id, connection); return Disposable.create(() => { diff --git a/packages/remote/src/electron-node/remote-tunnel-service.ts b/packages/remote/src/electron-node/remote-tunnel-service.ts deleted file mode 100644 index 50dd89c0abaed..0000000000000 --- a/packages/remote/src/electron-node/remote-tunnel-service.ts +++ /dev/null @@ -1,56 +0,0 @@ -// ***************************************************************************** -// Copyright (C) 2023 TypeFox and others. -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License v. 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0. -// -// This Source Code may also be made available under the following Secondary -// Licenses when the conditions for such availability set forth in the Eclipse -// Public License v. 2.0 are satisfied: GNU General Public License, version 2 -// with the GNU Classpath Exception which is available at -// https://www.gnu.org/software/classpath/license.html. -// -// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 -// ***************************************************************************** - -import { inject, injectable } from '@theia/core/shared/inversify'; -import * as net from 'net'; -import { RemoteConnectionService } from './remote-connection-service'; -import { RemoteProxyServerProvider } from './remote-proxy-server-provider'; -import { RemoteTunnel } from './remote-types'; - -export interface RemoteTunnelOptions { - remote: string; -} - -@injectable() -export class RemoteTunnelService { - - @inject(RemoteConnectionService) - protected readonly connectionService: RemoteConnectionService; - - @inject(RemoteProxyServerProvider) - protected readonly serverProvider: RemoteProxyServerProvider; - - async addTunnel(options: RemoteTunnelOptions): Promise { - const connection = this.connectionService.getConnection(options.remote); - if (!connection) { - throw new Error('No remote connection found for id ' + options.remote); - } - const server = await this.serverProvider.getProxyServer(socket => connection.forwardOut(socket)); - const port = (server.address() as net.AddressInfo).port; - const tunnel = new RemoteTunnel({ - port - }); - // When the frontend socket disconnects, close the server - tunnel.onDidSocketDisconnect(() => { - server.close(); - }); - connection.onDidDisconnect(() => { - tunnel?.disconnect(); - }); - return tunnel; - } - -} diff --git a/packages/remote/src/electron-node/remote-types.ts b/packages/remote/src/electron-node/remote-types.ts index 0e93e33ea3790..7499fd682004d 100644 --- a/packages/remote/src/electron-node/remote-types.ts +++ b/packages/remote/src/electron-node/remote-types.ts @@ -14,9 +14,14 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { Disposable, Emitter, Event } from '@theia/core'; +import { Disposable, Event, OS } from '@theia/core'; import * as net from 'net'; +export interface RemotePlatform { + os: OS.Type + arch: string +} + export type RemoteStatusReport = (message: string) => void; export interface ExpressLayer { @@ -49,35 +54,3 @@ export interface RemoteConnection extends Disposable { execPartial(cmd: string, tester: RemoteExecTester, args?: string[], options?: RemoteExecOptions): Promise; copy(localPath: string | Buffer | NodeJS.ReadableStream, remotePath: string): Promise; } - -export interface RemoteSessionOptions { - port: number; -} - -export class RemoteTunnel implements Disposable { - - readonly port: number; - - private readonly onDidRemoteDisconnectEmitter = new Emitter(); - private readonly onDidSocketDisconnectEmitter = new Emitter(); - - get onDidRemoteDisconnect(): Event { - return this.onDidRemoteDisconnectEmitter.event; - } - - get onDidSocketDisconnect(): Event { - return this.onDidSocketDisconnectEmitter.event; - } - - constructor(options: RemoteSessionOptions) { - this.port = options.port; - } - - disconnect(): void { - this.onDidRemoteDisconnectEmitter.fire(); - } - - dispose(): void { - this.onDidSocketDisconnectEmitter.fire(); - } -} diff --git a/packages/core/src/node/remote/app-native-dependency-contribution.ts b/packages/remote/src/electron-node/setup/app-native-dependency-contribution.ts similarity index 82% rename from packages/core/src/node/remote/app-native-dependency-contribution.ts rename to packages/remote/src/electron-node/setup/app-native-dependency-contribution.ts index 7cc0b8a401920..0dea1b5f3b722 100644 --- a/packages/core/src/node/remote/app-native-dependency-contribution.ts +++ b/packages/remote/src/electron-node/setup/app-native-dependency-contribution.ts @@ -14,8 +14,9 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { injectable } from 'inversify'; -import { RemoteNativeDependencyContribution, DownloadOptions, DependencyDownload, RemotePlatform } from './remote-native-dependency-contribution'; +import { injectable } from '@theia/core/shared/inversify'; +import { RemoteNativeDependencyContribution, DownloadOptions, DependencyDownload } from './remote-native-dependency-contribution'; +import { RemotePlatform } from '../remote-types'; @injectable() export class AppNativeDependencyContribution implements RemoteNativeDependencyContribution { @@ -25,8 +26,8 @@ export class AppNativeDependencyContribution implements RemoteNativeDependencyCo // 'https://github.com/eclipse-theia/theia/releases/download' appDownloadUrlBase = 'https://github.com/msujew/theia/releases/download'; - getDefaultURLForFile(remotePlatform: RemotePlatform, theiaVersion: string): string { - return `${this.appDownloadUrlBase}/v${theiaVersion}/native-dependencies-${remotePlatform}-x64.zip`; + protected getDefaultURLForFile(remotePlatform: RemotePlatform, theiaVersion: string): string { + return `${this.appDownloadUrlBase}/v${theiaVersion}/native-dependencies-${remotePlatform.os}-${remotePlatform.arch}.zip`; } async download(options: DownloadOptions): Promise { diff --git a/packages/core/src/node/remote/core-copy-contribution.ts b/packages/remote/src/electron-node/setup/main-copy-contribution.ts similarity index 90% rename from packages/core/src/node/remote/core-copy-contribution.ts rename to packages/remote/src/electron-node/setup/main-copy-contribution.ts index 4599acfadb767..f917bc678aa8b 100644 --- a/packages/core/src/node/remote/core-copy-contribution.ts +++ b/packages/remote/src/electron-node/setup/main-copy-contribution.ts @@ -14,11 +14,11 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { injectable } from 'inversify'; +import { injectable } from '@theia/core/shared/inversify'; import { RemoteCopyContribution, RemoteCopyRegistry } from './remote-copy-contribution'; @injectable() -export class CoreCopyContribution implements RemoteCopyContribution { +export class MainCopyContribution implements RemoteCopyContribution { async copy(registry: RemoteCopyRegistry): Promise { registry.file('package.json'); await registry.glob('lib/backend/!(native)'); diff --git a/packages/core/src/node/remote/remote-copy-contribution.ts b/packages/remote/src/electron-node/setup/remote-copy-contribution.ts similarity index 88% rename from packages/core/src/node/remote/remote-copy-contribution.ts rename to packages/remote/src/electron-node/setup/remote-copy-contribution.ts index e41afdf9011b1..b6db142310ea8 100644 --- a/packages/core/src/node/remote/remote-copy-contribution.ts +++ b/packages/remote/src/electron-node/setup/remote-copy-contribution.ts @@ -14,12 +14,12 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { ApplicationPackage } from '@theia/application-package'; -import { inject, injectable } from 'inversify'; +import { ApplicationPackage } from '@theia/core/shared/@theia/application-package'; +import { inject, injectable } from '@theia/core/shared/inversify'; import { glob as globCallback } from 'glob'; -import { MaybePromise } from '../../common'; import { promisify } from 'util'; import * as path from 'path'; +import { MaybePromise } from '@theia/core'; const promiseGlob = promisify(globCallback); @@ -30,6 +30,11 @@ export interface RemoteCopyContribution { } export interface RemoteCopyOptions { + /** + * The mode that the file should be set to once copied to the remote. + * + * Only relevant for POSIX-like systems + */ mode?: number; } diff --git a/packages/remote/src/electron-node/setup/remote-copy-service.ts b/packages/remote/src/electron-node/setup/remote-copy-service.ts index 09d9355e3d206..ffe46d40f99e4 100644 --- a/packages/remote/src/electron-node/setup/remote-copy-service.ts +++ b/packages/remote/src/electron-node/setup/remote-copy-service.ts @@ -14,16 +14,16 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { ApplicationPackage } from '@theia/core/shared/@theia/application-package'; -import { inject, injectable, named } from '@theia/core/shared/inversify'; -import { RemotePlatform, RemoteCopyContribution, RemoteCopyRegistry, RemoteFile } from '@theia/core/lib/node/remote'; -import { RemoteConnection } from '../remote-types'; -import { RemoteNativeDependencyService } from './remote-native-dependency-service'; -import { ContributionProvider } from '@theia/core'; import * as archiver from 'archiver'; import * as path from 'path'; import * as fs from 'fs'; import * as os from 'os'; +import { ApplicationPackage } from '@theia/core/shared/@theia/application-package'; +import { inject, injectable, named } from '@theia/core/shared/inversify'; +import { RemoteConnection, RemotePlatform } from '../remote-types'; +import { RemoteNativeDependencyService } from './remote-native-dependency-service'; +import { ContributionProvider } from '@theia/core'; +import { RemoteCopyContribution, RemoteCopyRegistry, RemoteFile } from './remote-copy-contribution'; @injectable() export class RemoteCopyService { diff --git a/packages/core/src/node/remote/remote-native-dependency-contribution.ts b/packages/remote/src/electron-node/setup/remote-native-dependency-contribution.ts similarity index 83% rename from packages/core/src/node/remote/remote-native-dependency-contribution.ts rename to packages/remote/src/electron-node/setup/remote-native-dependency-contribution.ts index 55d1c3deed36f..a13bb41b63d83 100644 --- a/packages/core/src/node/remote/remote-native-dependency-contribution.ts +++ b/packages/remote/src/electron-node/setup/remote-native-dependency-contribution.ts @@ -14,23 +14,15 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { RequestOptions } from '@theia/request'; -import { isObject } from '../../common'; +import { isObject } from '@theia/core'; +import { RequestOptions } from '@theia/core/shared/@theia/request'; +import { RemotePlatform } from '../remote-types'; export interface FileDependencyResult { path: string; mode?: number; } -export type RemotePlatform = 'windows' | 'linux' | 'darwin'; - -export namespace RemotePlatform { - export function joinPath(platform: RemotePlatform, ...segments: string[]): string { - const separator = platform === 'windows' ? '\\' : '/'; - return segments.join(separator); - } -} - export type DependencyDownload = FileDependencyDownload | DirectoryDependencyDownload; export interface FileDependencyDownload { @@ -56,7 +48,7 @@ export namespace DirectoryDependencyDownload { } export interface DownloadOptions { - remotePlatform: RemotePlatform; + remotePlatform: RemotePlatform theiaVersion: string; download: (requestInfo: string | RequestOptions) => Promise } diff --git a/packages/remote/src/electron-node/setup/remote-native-dependency-service.ts b/packages/remote/src/electron-node/setup/remote-native-dependency-service.ts index 4682bec2a9e87..6caa905cbdaa5 100644 --- a/packages/remote/src/electron-node/setup/remote-native-dependency-service.ts +++ b/packages/remote/src/electron-node/setup/remote-native-dependency-service.ts @@ -16,11 +16,12 @@ import { ContributionProvider, THEIA_VERSION } from '@theia/core'; import { inject, injectable, named } from '@theia/core/shared/inversify'; -import { DependencyDownload, DirectoryDependencyDownload, RemoteNativeDependencyContribution, RemotePlatform } from '@theia/core/lib/node/remote'; import { RequestContext, RequestService, RequestOptions } from '@theia/core/shared/@theia/request'; import * as decompress from 'decompress'; import * as path from 'path'; import * as fs from 'fs/promises'; +import { DependencyDownload, DirectoryDependencyDownload, RemoteNativeDependencyContribution } from './remote-native-dependency-contribution'; +import { RemotePlatform } from '../remote-types'; const decompressTar = require('decompress-tar'); const decompressTargz = require('decompress-targz'); diff --git a/packages/remote/src/electron-node/setup/remote-node-setup-service.ts b/packages/remote/src/electron-node/setup/remote-node-setup-service.ts index 892d086ba119d..acee2f787f772 100644 --- a/packages/remote/src/electron-node/setup/remote-node-setup-service.ts +++ b/packages/remote/src/electron-node/setup/remote-node-setup-service.ts @@ -19,9 +19,10 @@ import * as fs from '@theia/core/shared/fs-extra'; import * as os from 'os'; import { inject, injectable } from '@theia/core/shared/inversify'; -import { RemotePlatform } from '@theia/core/lib/node/remote'; import { RequestService } from '@theia/core/shared/@theia/request'; import { RemoteSetupScriptService } from './remote-setup-script-service'; +import { RemotePlatform } from '../remote-types'; +import { OS } from '@theia/core'; /** * The current node version that Theia recommends. @@ -40,7 +41,9 @@ export class RemoteNodeSetupService { protected readonly scriptService: RemoteSetupScriptService; getNodeDirectoryName(platform: RemotePlatform): string { - const platformId = platform === 'windows' ? 'win' : platform; + const platformId = + platform.os === OS.Type.Windows ? 'win' : + platform.os === OS.Type.Linux ? 'linux' : 'darwin'; // Always use x64 architecture for now const arch = 'x64'; const dirName = `node-v${REMOTE_NODE_VERSION}-${platformId}-${arch}`; @@ -49,9 +52,9 @@ export class RemoteNodeSetupService { getNodeFileName(platform: RemotePlatform): string { let fileExtension = ''; - if (platform === 'windows') { + if (platform.os === OS.Type.Windows) { fileExtension = 'zip'; - } else if (platform === 'darwin') { + } else if (platform.os === OS.Type.OSX) { fileExtension = 'tar.gz'; } else { fileExtension = 'tar.xz'; @@ -76,9 +79,9 @@ export class RemoteNodeSetupService { generateDownloadScript(platform: RemotePlatform, targetPath: string): string { const fileName = this.getNodeFileName(platform); const downloadPath = this.getDownloadPath(fileName); - const zipPath = RemotePlatform.joinPath(platform, targetPath, fileName); + const zipPath = this.scriptService.joinPath(platform, targetPath, fileName); const download = this.scriptService.downloadFile(platform, downloadPath, zipPath); - const unzip = this.scriptService.unzip(zipPath, targetPath); + const unzip = this.scriptService.unzip(platform, zipPath, targetPath); return this.scriptService.joinScript(platform, download, unzip); } diff --git a/packages/remote/src/electron-node/setup/remote-setup-script-service.ts b/packages/remote/src/electron-node/setup/remote-setup-script-service.ts index e3af8de6340a2..a4d576ed37ae8 100644 --- a/packages/remote/src/electron-node/setup/remote-setup-script-service.ts +++ b/packages/remote/src/electron-node/setup/remote-setup-script-service.ts @@ -14,17 +14,65 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { injectable } from '@theia/core/shared/inversify'; -import { RemotePlatform } from '@theia/core/lib/node/remote'; +import { OS } from '@theia/core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { RemotePlatform } from '../remote-types'; + +export interface RemoteScriptStrategy { + exec(): string; + downloadFile(url: string, output: string): string; + unzip(file: string, directory: string): string; + mkdir(path: string): string; + home(): string; + joinPath(...segments: string[]): string; + joinScript(...segments: string[]): string; +} @injectable() -export class RemoteSetupScriptService { +export class RemoteWindowsScriptStrategy implements RemoteScriptStrategy { - downloadFile(platform: RemotePlatform, url: string, output: string): string { - if (platform === 'windows') { - return `Invoke-WebRequest -Uri "${url}" -OutFile ${output}`; - } else { - return ` + home(): string { + return 'PowerShell -Command $HOME'; + } + + exec(): string { + return 'PowerShell -Command'; + } + + downloadFile(url: string, output: string): string { + return `Invoke-WebRequest -Uri "${url}" -OutFile ${output}`; + } + + unzip(file: string, directory: string): string { + return `tar -xf "${file}" -C "${directory}"`; + } + + mkdir(path: string): string { + return `New-Item -Force -itemType Directory -Path "${path}"`; + } + + joinPath(...segments: string[]): string { + return segments.join('\\'); + } + + joinScript(...segments: string[]): string { + return segments.join('\r\n'); + } +} + +@injectable() +export class RemotePosixScriptStrategy implements RemoteScriptStrategy { + + home(): string { + return 'eval echo ~'; + } + + exec(): string { + return 'sh -c'; + } + + downloadFile(url: string, output: string): string { + return ` if [ "$(command -v wget)" ]; then echo "Downloading using wget" wget -O "${output}" "${url}" @@ -36,22 +84,63 @@ else exit 1 fi `.trim(); - } } unzip(file: string, directory: string): string { return `tar -xf "${file}" -C "${directory}"`; } + mkdir(path: string): string { + return `mkdir -p "${path}"`; + } + + joinPath(...segments: string[]): string { + return segments.join('/'); + } + + joinScript(...segments: string[]): string { + return segments.join('\n'); + } +} + +@injectable() +export class RemoteSetupScriptService { + + @inject(RemoteWindowsScriptStrategy) + protected windowsStrategy: RemoteWindowsScriptStrategy; + + @inject(RemotePosixScriptStrategy) + protected posixStrategy: RemotePosixScriptStrategy; + + protected getStrategy(platform: RemotePlatform): RemoteScriptStrategy { + return platform.os === OS.Type.Windows ? this.windowsStrategy : this.posixStrategy; + } + + home(platform: RemotePlatform): string { + return this.getStrategy(platform).home(); + } + + exec(platform: RemotePlatform): string { + return this.getStrategy(platform).exec(); + } + + downloadFile(platform: RemotePlatform, url: string, output: string): string { + return this.getStrategy(platform).downloadFile(url, output); + } + + unzip(platform: RemotePlatform, file: string, directory: string): string { + return this.getStrategy(platform).unzip(file, directory); + } + mkdir(platform: RemotePlatform, path: string): string { - if (platform === 'windows') { - return `New-Item -Force -itemType Directory -Path "${path}"`; - } else { - return `mkdir -p "${path}"`; - } + return this.getStrategy(platform).mkdir(path); + } + + joinPath(platform: RemotePlatform, ...segments: string[]): string { + return this.getStrategy(platform).joinPath(...segments); } joinScript(platform: RemotePlatform, ...segments: string[]): string { - return segments.join(platform === 'windows' ? '\r\n' : '\n'); + return this.getStrategy(platform).joinScript(...segments); } } diff --git a/packages/remote/src/electron-node/setup/remote-setup-service.ts b/packages/remote/src/electron-node/setup/remote-setup-service.ts index e3e312ccd3b76..4cbd3e1b20478 100644 --- a/packages/remote/src/electron-node/setup/remote-setup-service.ts +++ b/packages/remote/src/electron-node/setup/remote-setup-service.ts @@ -15,13 +15,12 @@ // ***************************************************************************** import { inject, injectable } from '@theia/core/shared/inversify'; -import { RemoteConnection, RemoteExecResult, RemoteStatusReport } from '../remote-types'; +import { RemoteConnection, RemoteExecResult, RemotePlatform, RemoteStatusReport } from '../remote-types'; import { ApplicationPackage } from '@theia/core/shared/@theia/application-package'; import { RemoteCopyService } from './remote-copy-service'; import { RemoteNativeDependencyService } from './remote-native-dependency-service'; -import { THEIA_VERSION } from '@theia/core'; +import { OS, THEIA_VERSION } from '@theia/core'; import { RemoteNodeSetupService } from './remote-node-setup-service'; -import { RemotePlatform } from '@theia/core/lib/node/remote'; import { RemoteSetupScriptService } from './remote-setup-script-service'; @injectable() @@ -48,29 +47,29 @@ export class RemoteSetupService { const platform = await this.detectRemotePlatform(connection); // 2. Setup home directory const remoteHome = await this.getRemoteHomeDirectory(connection, platform); - const applicationDirectory = RemotePlatform.joinPath(platform, remoteHome, `.${this.getRemoteAppName()}`); + const applicationDirectory = this.scriptService.joinPath(platform, remoteHome, `.${this.getRemoteAppName()}`); await this.mkdirRemote(connection, platform, applicationDirectory); // 3. Download+copy node for that platform const nodeFileName = this.nodeSetupService.getNodeFileName(platform); const nodeDirName = this.nodeSetupService.getNodeDirectoryName(platform); - const remoteNodeDirectory = RemotePlatform.joinPath(platform, applicationDirectory, nodeDirName); + const remoteNodeDirectory = this.scriptService.joinPath(platform, applicationDirectory, nodeDirName); const nodeDirExists = await this.dirExistsRemote(connection, remoteNodeDirectory); if (!nodeDirExists) { report('Downloading and installing Node.js on remote...'); // Download the binaries locally and move it via SSH const nodeArchive = await this.nodeSetupService.downloadNode(platform); - const remoteNodeZip = RemotePlatform.joinPath(platform, applicationDirectory, nodeFileName); + const remoteNodeZip = this.scriptService.joinPath(platform, applicationDirectory, nodeFileName); await connection.copy(nodeArchive, remoteNodeZip); - await this.unzipRemote(connection, remoteNodeZip, applicationDirectory); + await this.unzipRemote(connection, platform, remoteNodeZip, applicationDirectory); } // 4. Copy backend to remote system - const libDir = RemotePlatform.joinPath(platform, applicationDirectory, 'lib'); + const libDir = this.scriptService.joinPath(platform, applicationDirectory, 'lib'); const libDirExists = await this.dirExistsRemote(connection, libDir); if (!libDirExists) { report('Installing application on remote...'); - const applicationZipFile = RemotePlatform.joinPath(platform, applicationDirectory, `${this.getRemoteAppName()}.tar`); + const applicationZipFile = this.scriptService.joinPath(platform, applicationDirectory, `${this.getRemoteAppName()}.tar`); await this.copyService.copyToRemote(connection, platform, applicationZipFile); - await this.unzipRemote(connection, applicationZipFile, applicationDirectory); + await this.unzipRemote(connection, platform, applicationZipFile, applicationDirectory); } // 5. start remote backend report('Starting application on remote...'); @@ -79,8 +78,8 @@ export class RemoteSetupService { } protected async startApplication(connection: RemoteConnection, platform: RemotePlatform, remotePath: string, nodeDir: string): Promise { - const nodeExecutable = RemotePlatform.joinPath(platform, nodeDir, 'bin', platform === 'windows' ? 'node.exe' : 'node'); - const mainJsFile = RemotePlatform.joinPath(platform, remotePath, 'lib', 'backend', 'main.js'); + const nodeExecutable = this.scriptService.joinPath(platform, nodeDir, 'bin', platform.os === OS.Type.Windows ? 'node.exe' : 'node'); + const mainJsFile = this.scriptService.joinPath(platform, remotePath, 'lib', 'backend', 'main.js'); const localAddressRegex = /listening on http:\/\/127.0.0.1:(\d+)/; // Change to the remote application path and start a node process with the copied main.js file // This way, our current working directory is set as expected @@ -97,31 +96,54 @@ export class RemoteSetupService { } protected async detectRemotePlatform(connection: RemoteConnection): Promise { - const result = await connection.exec('uname -s'); + const osResult = await connection.exec('uname -s'); - if (result.stderr) { + let os: OS.Type | undefined; + if (osResult.stderr) { // Only Windows systems return an error output here - return 'windows'; - } else if (result.stdout) { - if (result.stdout.includes('windows32') || result.stdout.includes('MINGW64')) { - return 'windows'; - } else if (result.stdout.includes('Linux')) { - return 'linux'; - } else if (result.stdout.includes('Darwin')) { - return 'darwin'; + os = OS.Type.Windows; + } else if (osResult.stdout) { + if (osResult.stdout.includes('windows32') || osResult.stdout.includes('MINGW64')) { + os = OS.Type.Windows; + } else if (osResult.stdout.includes('Linux')) { + os = OS.Type.Linux; + } else if (osResult.stdout.includes('Darwin')) { + os = OS.Type.OSX; + } + } + if (os === undefined) { + throw new Error('Failed to identify remote system: ' + osResult.stdout + '\n' + osResult.stderr); + } + let arch: string | undefined; + if (os === OS.Type.Windows) { + const wmicResult = await connection.exec('wmic OS get OSArchitecture'); + if (wmicResult.stdout.includes('64-bit')) { + arch = 'x64'; + } else if (wmicResult.stdout.includes('32-bit')) { + arch = 'x86'; + } + } else { + const archResult = (await connection.exec('uname -m')).stdout; + if (archResult.includes('x86_64')) { + arch = 'x64'; + } else if (archResult.match(/i\d83/)) { // i386, i483, i683 + arch = 'x32'; + } else { + arch = archResult.trim(); } } - throw new Error('Failed to identify remote system: ' + result.stdout + '\n' + result.stderr); + if (!arch) { + throw new Error('Could not identify remote system architecture'); + } + return { + os, + arch + }; } protected async getRemoteHomeDirectory(connection: RemoteConnection, platform: RemotePlatform): Promise { - if (platform === 'windows') { - const powershellHome = await connection.exec('PowerShell -Command $HOME'); - return powershellHome.stdout.trim(); - } else { - const result = await connection.exec('eval echo ~'); - return result.stdout.trim(); - } + const result = await connection.exec(this.scriptService.home(platform)); + return result.stdout.trim(); } protected getRemoteAppName(): string { @@ -146,18 +168,14 @@ export class RemoteSetupService { return !Boolean(cdResult.stderr); } - protected async unzipRemote(connection: RemoteConnection, remoteFile: string, remoteDirectory: string): Promise { - const result = await connection.exec(this.scriptService.unzip(remoteFile, remoteDirectory)); + protected async unzipRemote(connection: RemoteConnection, platform: RemotePlatform, remoteFile: string, remoteDirectory: string): Promise { + const result = await connection.exec(this.scriptService.unzip(platform, remoteFile, remoteDirectory)); if (result.stderr) { throw new Error('Failed to unzip: ' + result.stderr); } } protected async executeScriptRemote(connection: RemoteConnection, platform: RemotePlatform, script: string): Promise { - if (platform === 'windows') { - return connection.exec('PowerShell -Command', [script]); - } else { - return connection.exec('sh -c', [script]); - } + return connection.exec(this.scriptService.exec(platform), [script]); } } diff --git a/packages/remote/src/electron-node/ssh/remote-ssh-connection-provider.ts b/packages/remote/src/electron-node/ssh/remote-ssh-connection-provider.ts index f0b944ef4867a..46c177c5db112 100644 --- a/packages/remote/src/electron-node/ssh/remote-ssh-connection-provider.ts +++ b/packages/remote/src/electron-node/ssh/remote-ssh-connection-provider.ts @@ -14,19 +14,20 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** +import * as ssh2 from 'ssh2'; +import * as net from 'net'; import * as fs from '@theia/core/shared/fs-extra'; +import SftpClient = require('ssh2-sftp-client'); import { Emitter, Event, MessageService, QuickInputService } from '@theia/core'; import { inject, injectable } from '@theia/core/shared/inversify'; import { RemoteSSHConnectionProvider } from '../../electron-common/remote-ssh-connection-provider'; import { RemoteConnectionService } from '../remote-connection-service'; import { RemoteProxyServerProvider } from '../remote-proxy-server-provider'; import { RemoteConnection, RemoteExecOptions, RemoteExecResult, RemoteExecTester, RemoteStatusReport } from '../remote-types'; -import * as ssh2 from 'ssh2'; -import SftpClient = require('ssh2-sftp-client'); -import * as net from 'net'; import { Deferred, timeout } from '@theia/core/lib/common/promise-util'; import { SSHIdentityFileCollector, SSHKey } from './ssh-identity-file-collector'; import { RemoteSetupService } from '../setup/remote-setup-service'; +import { v4 } from 'uuid'; @injectable() export class RemoteSSHConnectionProviderImpl implements RemoteSSHConnectionProvider { @@ -69,6 +70,13 @@ export class RemoteSSHConnectionProviderImpl implements RemoteSSHConnectionProvi server.close(); registration.dispose(); }); + server.on('connection', socket => { + // This event is triggered once the frontend connects to the proxy server. + // When the connection drops, we need to dispose the remote connection + socket.once('close', () => { + remote.dispose(); + }); + }); const localPort = (server.address() as net.AddressInfo).port; remote.localPort = localPort; return localPort.toString(); @@ -79,7 +87,6 @@ export class RemoteSSHConnectionProviderImpl implements RemoteSSHConnectionProvi async establishSSHConnection(host: string, user: string): Promise { const deferred = new Deferred(); - const sessionId = this.remoteConnectionService.getConnectionId(); const sshClient = new ssh2.Client(); const identityFiles = await this.identityFileCollector.gatherIdentityFiles(); const sshAuthHandler = this.getAuthHandler(user, host, identityFiles); @@ -87,7 +94,7 @@ export class RemoteSSHConnectionProviderImpl implements RemoteSSHConnectionProvi .on('ready', async () => { const connection = new RemoteSSHConnection({ client: sshClient, - id: sessionId, + id: v4(), name: host, type: 'SSH' }); @@ -97,6 +104,8 @@ export class RemoteSSHConnectionProviderImpl implements RemoteSSHConnectionProvi } catch (err) { deferred.reject(err); } + }).on('end', () => { + console.log(`Ended remote connection to host '${user}@${host}'`); }).on('error', err => { deferred.reject(err); }).connect({ @@ -154,7 +163,7 @@ export class RemoteSSHConnectionProviderImpl implements RemoteSSHConnectionProvi const keyBuffer = await fs.promises.readFile(identityKey.filename); let result = ssh2.utils.parseKey(keyBuffer); // First try without passphrase - if (result instanceof Error && result.message === 'Encrypted private OpenSSH key detected, but no passphrase given') { + if (result instanceof Error && result.message.match(/no passphrase given/)) { let passphraseRetryCount = this.passphraseRetryCount; while (result instanceof Error && passphraseRetryCount > 0) { const passphrase = await this.quickInputService.input({ From 769f9c6178e2df92299f6ebc4812cfb1af78eaa8 Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Mon, 23 Oct 2023 19:51:09 +0200 Subject: [PATCH 03/12] Add platform architecture validation --- .../app-native-dependency-contribution.ts | 3 ++ .../setup/remote-node-setup-service.ts | 33 +++++++++++++++---- .../setup/remote-setup-service.ts | 6 ++-- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/packages/remote/src/electron-node/setup/app-native-dependency-contribution.ts b/packages/remote/src/electron-node/setup/app-native-dependency-contribution.ts index 0dea1b5f3b722..265bd16635f17 100644 --- a/packages/remote/src/electron-node/setup/app-native-dependency-contribution.ts +++ b/packages/remote/src/electron-node/setup/app-native-dependency-contribution.ts @@ -27,6 +27,9 @@ export class AppNativeDependencyContribution implements RemoteNativeDependencyCo appDownloadUrlBase = 'https://github.com/msujew/theia/releases/download'; protected getDefaultURLForFile(remotePlatform: RemotePlatform, theiaVersion: string): string { + if (remotePlatform.arch !== 'x64') { + throw new Error(`Unsupported remote architecture '${remotePlatform.arch}'. Remote support is only available for x64 architectures.`); + } return `${this.appDownloadUrlBase}/v${theiaVersion}/native-dependencies-${remotePlatform.os}-${remotePlatform.arch}.zip`; } diff --git a/packages/remote/src/electron-node/setup/remote-node-setup-service.ts b/packages/remote/src/electron-node/setup/remote-node-setup-service.ts index acee2f787f772..b7694779fb5b8 100644 --- a/packages/remote/src/electron-node/setup/remote-node-setup-service.ts +++ b/packages/remote/src/electron-node/setup/remote-node-setup-service.ts @@ -41,17 +41,34 @@ export class RemoteNodeSetupService { protected readonly scriptService: RemoteSetupScriptService; getNodeDirectoryName(platform: RemotePlatform): string { - const platformId = - platform.os === OS.Type.Windows ? 'win' : - platform.os === OS.Type.Linux ? 'linux' : 'darwin'; - // Always use x64 architecture for now - const arch = 'x64'; - const dirName = `node-v${REMOTE_NODE_VERSION}-${platformId}-${arch}`; + let platformId: string; + if (platform.os === OS.Type.Windows) { + platformId = 'win'; + } else if (platform.os === OS.Type.OSX) { + platformId = 'darwin'; + } else { + platformId = 'linux'; + } + const dirName = `node-v${REMOTE_NODE_VERSION}-${platformId}-${platform.arch}`; return dirName; } + protected validatePlatform(platform: RemotePlatform): void { + if (platform.os === OS.Type.Windows && !platform.arch.match(/^x(64|86)$/)) { + this.throwPlatformError(platform, 'x64 and x86'); + } else if (platform.os === OS.Type.Linux && !platform.arch.match(/^(x64|armv7l|arm64)$/)) { + this.throwPlatformError(platform, 'x64, armv7l and arm64'); + } else if (platform.os === OS.Type.Linux && !platform.arch.match(/^(x64|arm64)$/)) { + this.throwPlatformError(platform, 'x64 and arm64'); + } + } + + protected throwPlatformError(platform: RemotePlatform, supportedArch: string): never { + throw new Error(`Invalid architecture for ${platform.os}: '${platform.arch}'. Only ${supportedArch} are supported.`); + } + getNodeFileName(platform: RemotePlatform): string { - let fileExtension = ''; + let fileExtension: string; if (platform.os === OS.Type.Windows) { fileExtension = 'zip'; } else if (platform.os === OS.Type.OSX) { @@ -63,6 +80,7 @@ export class RemoteNodeSetupService { } async downloadNode(platform: RemotePlatform): Promise { + this.validatePlatform(platform); const fileName = this.getNodeFileName(platform); const tmpdir = os.tmpdir(); const localPath = path.join(tmpdir, fileName); @@ -77,6 +95,7 @@ export class RemoteNodeSetupService { } generateDownloadScript(platform: RemotePlatform, targetPath: string): string { + this.validatePlatform(platform); const fileName = this.getNodeFileName(platform); const downloadPath = this.getDownloadPath(fileName); const zipPath = this.scriptService.joinPath(platform, targetPath, fileName); diff --git a/packages/remote/src/electron-node/setup/remote-setup-service.ts b/packages/remote/src/electron-node/setup/remote-setup-service.ts index 4cbd3e1b20478..2f78e08ae0665 100644 --- a/packages/remote/src/electron-node/setup/remote-setup-service.ts +++ b/packages/remote/src/electron-node/setup/remote-setup-service.ts @@ -100,7 +100,7 @@ export class RemoteSetupService { let os: OS.Type | undefined; if (osResult.stderr) { - // Only Windows systems return an error output here + // Only Windows systems return an error stream here os = OS.Type.Windows; } else if (osResult.stdout) { if (osResult.stdout.includes('windows32') || osResult.stdout.includes('MINGW64')) { @@ -111,7 +111,7 @@ export class RemoteSetupService { os = OS.Type.OSX; } } - if (os === undefined) { + if (!os) { throw new Error('Failed to identify remote system: ' + osResult.stdout + '\n' + osResult.stderr); } let arch: string | undefined; @@ -127,7 +127,7 @@ export class RemoteSetupService { if (archResult.includes('x86_64')) { arch = 'x64'; } else if (archResult.match(/i\d83/)) { // i386, i483, i683 - arch = 'x32'; + arch = 'x86'; } else { arch = archResult.trim(); } From c6513314e1d3d9a3b08f5e948b3403eee4492849 Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Wed, 25 Oct 2023 11:53:27 +0200 Subject: [PATCH 04/12] Fix incorrect mac error --- .../remote/src/electron-node/setup/remote-node-setup-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/remote/src/electron-node/setup/remote-node-setup-service.ts b/packages/remote/src/electron-node/setup/remote-node-setup-service.ts index b7694779fb5b8..a7b6768b39882 100644 --- a/packages/remote/src/electron-node/setup/remote-node-setup-service.ts +++ b/packages/remote/src/electron-node/setup/remote-node-setup-service.ts @@ -58,7 +58,7 @@ export class RemoteNodeSetupService { this.throwPlatformError(platform, 'x64 and x86'); } else if (platform.os === OS.Type.Linux && !platform.arch.match(/^(x64|armv7l|arm64)$/)) { this.throwPlatformError(platform, 'x64, armv7l and arm64'); - } else if (platform.os === OS.Type.Linux && !platform.arch.match(/^(x64|arm64)$/)) { + } else if (platform.os === OS.Type.OSX && !platform.arch.match(/^(x64|arm64)$/)) { this.throwPlatformError(platform, 'x64 and arm64'); } } From 604a52d53d9dfd664e565d801ea7f4c572895997 Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Wed, 25 Oct 2023 13:13:30 +0200 Subject: [PATCH 05/12] Use preferences for node download template --- .../remote-frontend-module.ts | 3 + .../electron-browser/remote-preferences.ts | 62 +++++++++++++++++++ .../remote-ssh-contribution.ts | 10 ++- .../remote-ssh-connection-provider.ts | 10 +-- .../setup/remote-node-setup-service.ts | 34 ++++++---- .../setup/remote-setup-service.ts | 15 ++++- .../ssh/remote-ssh-connection-provider.ts | 17 +++-- packages/remote/tsconfig.json | 3 + yarn.lock | 5 -- 9 files changed, 126 insertions(+), 33 deletions(-) create mode 100644 packages/remote/src/electron-browser/remote-preferences.ts diff --git a/packages/remote/src/electron-browser/remote-frontend-module.ts b/packages/remote/src/electron-browser/remote-frontend-module.ts index 91269cb796b15..f2ddaf3d9e2d4 100644 --- a/packages/remote/src/electron-browser/remote-frontend-module.ts +++ b/packages/remote/src/electron-browser/remote-frontend-module.ts @@ -25,6 +25,7 @@ import { RemoteService } from './remote-service'; import { RemoteStatusService, RemoteStatusServicePath } from '../electron-common/remote-status-service'; import { ElectronFileDialogService } from '@theia/filesystem/lib/electron-browser/file-dialog/electron-file-dialog-service'; import { RemoteElectronFileDialogService } from './remote-electron-file-dialog-service'; +import { bindRemotePreferences } from './remote-preferences'; export default new ContainerModule((bind, _, __, rebind) => { bind(RemoteFrontendContribution).toSelf().inSingletonScope(); @@ -35,6 +36,8 @@ export default new ContainerModule((bind, _, __, rebind) => { bind(RemoteSSHContribution).toSelf().inSingletonScope(); bind(RemoteRegistryContribution).toService(RemoteSSHContribution); + bindRemotePreferences(bind); + rebind(ElectronFileDialogService).to(RemoteElectronFileDialogService).inSingletonScope(); bind(RemoteService).toSelf().inSingletonScope(); diff --git a/packages/remote/src/electron-browser/remote-preferences.ts b/packages/remote/src/electron-browser/remote-preferences.ts new file mode 100644 index 0000000000000..b9078d3207a09 --- /dev/null +++ b/packages/remote/src/electron-browser/remote-preferences.ts @@ -0,0 +1,62 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { interfaces } from '@theia/core/shared/inversify'; +import { + PreferenceProxy, + PreferenceSchema, + PreferenceContribution +} from '@theia/core/lib/browser/preferences'; +import { nls } from '@theia/core/lib/common/nls'; +import { PreferenceProxyFactory } from '@theia/core/lib/browser/preferences/injectable-preference-proxy'; + +const nodeDownloadTemplateParts = [ + nls.localize('theia/remote/nodeDownloadTemplateVersion', '`{version}` for the used node version'), + nls.localize('theia/remote/nodeDownloadTemplateOS', '`{os}` for the remote operating system. Either `win`, `linux` or `darwin`.'), + nls.localize('theia/remote/nodeDownloadTemplateArch', '`{arch}` for the remote system architecture.'), + nls.localize('theia/remote/nodeDownloadTemplateExt', '`{ext}` for the file extension. Either `zip`, `tar.xz` or `tar.xz`, depending on the operating system.') +]; + +export const RemotePreferenceSchema: PreferenceSchema = { + 'type': 'object', + properties: { + 'remote.nodeDownloadTemplate': { + type: 'string', + default: '', + markdownDescription: nls.localize( + 'theia/remote/nodeDownloadTemplate', + 'Controls the template used to download the node.js binaries for the remote backend. Points to the official node.js website by default. Uses multiple placeholders:' + ) + '\n- ' + nodeDownloadTemplateParts.join('\n- ') + }, + } +}; + +export interface RemoteConfiguration { + 'remote.nodeDownloadTemplate': string; +} + +export const RemotePreferenceContribution = Symbol('RemotePreferenceContribution'); +export const RemotePreferences = Symbol('GettingStartedPreferences'); +export type RemotePreferences = PreferenceProxy; + +export function bindRemotePreferences(bind: interfaces.Bind): void { + bind(RemotePreferences).toDynamicValue(ctx => { + const factory = ctx.container.get(PreferenceProxyFactory); + return factory(RemotePreferenceSchema); + }).inSingletonScope(); + bind(RemotePreferenceContribution).toConstantValue({ schema: RemotePreferenceSchema }); + bind(PreferenceContribution).toService(RemotePreferenceContribution); +} diff --git a/packages/remote/src/electron-browser/remote-ssh-contribution.ts b/packages/remote/src/electron-browser/remote-ssh-contribution.ts index 23fe5f6369e67..b3cb29f669c5b 100644 --- a/packages/remote/src/electron-browser/remote-ssh-contribution.ts +++ b/packages/remote/src/electron-browser/remote-ssh-contribution.ts @@ -18,6 +18,7 @@ import { Command, MessageService, nls, QuickInputService } from '@theia/core'; import { inject, injectable } from '@theia/core/shared/inversify'; import { RemoteSSHConnectionProvider } from '../electron-common/remote-ssh-connection-provider'; import { AbstractRemoteRegistryContribution, RemoteRegistry } from './remote-registry-contribution'; +import { RemotePreferences } from './remote-preferences'; export namespace RemoteSSHCommands { export const CONNECT: Command = Command.toLocalizedCommand({ @@ -44,6 +45,9 @@ export class RemoteSSHContribution extends AbstractRemoteRegistryContribution { @inject(MessageService) protected readonly messageService: MessageService; + @inject(RemotePreferences) + protected readonly remotePreferences: RemotePreferences; + registerRemoteCommands(registry: RemoteRegistry): void { registry.registerCommand(RemoteSSHCommands.CONNECT, { execute: () => this.connect(true) @@ -89,6 +93,10 @@ export class RemoteSSHContribution extends AbstractRemoteRegistryContribution { } async sendSSHConnect(host: string, user: string): Promise { - return this.sshConnectionProvider.establishConnection(host, user); + return this.sshConnectionProvider.establishConnection({ + host, + user, + nodeDownloadTemplate: this.remotePreferences['remote.nodeDownloadTemplate'] + }); } } diff --git a/packages/remote/src/electron-common/remote-ssh-connection-provider.ts b/packages/remote/src/electron-common/remote-ssh-connection-provider.ts index c1e7650151feb..f191e1ad2c583 100644 --- a/packages/remote/src/electron-common/remote-ssh-connection-provider.ts +++ b/packages/remote/src/electron-common/remote-ssh-connection-provider.ts @@ -18,12 +18,12 @@ export const RemoteSSHConnectionProviderPath = '/remote/ssh'; export const RemoteSSHConnectionProvider = Symbol('RemoteSSHConnectionProvider'); -export interface RemoteSSHConnectionOptions { - user?: string; - host?: string; +export interface RemoteSSHConnectionProviderOptions { + user: string; + host: string; + nodeDownloadTemplate?: string; } export interface RemoteSSHConnectionProvider { - establishConnection(host: string, user: string): Promise; - isConnectionAlive(remoteId: string): Promise; + establishConnection(options: RemoteSSHConnectionProviderOptions): Promise; } diff --git a/packages/remote/src/electron-node/setup/remote-node-setup-service.ts b/packages/remote/src/electron-node/setup/remote-node-setup-service.ts index a7b6768b39882..f559c2c494cde 100644 --- a/packages/remote/src/electron-node/setup/remote-node-setup-service.ts +++ b/packages/remote/src/electron-node/setup/remote-node-setup-service.ts @@ -41,6 +41,10 @@ export class RemoteNodeSetupService { protected readonly scriptService: RemoteSetupScriptService; getNodeDirectoryName(platform: RemotePlatform): string { + return `node-v${REMOTE_NODE_VERSION}-${this.getPlatformName(platform)}-${platform.arch}`; + } + + protected getPlatformName(platform: RemotePlatform): string { let platformId: string; if (platform.os === OS.Type.Windows) { platformId = 'win'; @@ -49,8 +53,7 @@ export class RemoteNodeSetupService { } else { platformId = 'linux'; } - const dirName = `node-v${REMOTE_NODE_VERSION}-${platformId}-${platform.arch}`; - return dirName; + return platformId; } protected validatePlatform(platform: RemotePlatform): void { @@ -67,7 +70,7 @@ export class RemoteNodeSetupService { throw new Error(`Invalid architecture for ${platform.os}: '${platform.arch}'. Only ${supportedArch} are supported.`); } - getNodeFileName(platform: RemotePlatform): string { + protected getNodeFileExtension(platform: RemotePlatform): string { let fileExtension: string; if (platform.os === OS.Type.Windows) { fileExtension = 'zip'; @@ -76,16 +79,20 @@ export class RemoteNodeSetupService { } else { fileExtension = 'tar.xz'; } - return `${this.getNodeDirectoryName(platform)}.${fileExtension}`; + return fileExtension; } - async downloadNode(platform: RemotePlatform): Promise { + getNodeFileName(platform: RemotePlatform): string { + return `${this.getNodeDirectoryName(platform)}.${this.getNodeFileExtension(platform)}`; + } + + async downloadNode(platform: RemotePlatform, downloadTemplate?: string): Promise { this.validatePlatform(platform); const fileName = this.getNodeFileName(platform); const tmpdir = os.tmpdir(); const localPath = path.join(tmpdir, fileName); if (!await fs.pathExists(localPath)) { - const downloadPath = this.getDownloadPath(fileName); + const downloadPath = this.getDownloadPath(platform, downloadTemplate); const downloadResult = await this.requestService.request({ url: downloadPath }); @@ -94,18 +101,23 @@ export class RemoteNodeSetupService { return localPath; } - generateDownloadScript(platform: RemotePlatform, targetPath: string): string { + generateDownloadScript(platform: RemotePlatform, targetPath: string, downloadTemplate?: string): string { this.validatePlatform(platform); const fileName = this.getNodeFileName(platform); - const downloadPath = this.getDownloadPath(fileName); + const downloadPath = this.getDownloadPath(platform, downloadTemplate); const zipPath = this.scriptService.joinPath(platform, targetPath, fileName); const download = this.scriptService.downloadFile(platform, downloadPath, zipPath); const unzip = this.scriptService.unzip(platform, zipPath, targetPath); return this.scriptService.joinScript(platform, download, unzip); } - protected getDownloadPath(fileName: string): string { - return `https://nodejs.org/dist/v${REMOTE_NODE_VERSION}/${fileName}`; + protected getDownloadPath(platform: RemotePlatform, downloadTemplate?: string): string { + const template = downloadTemplate || 'https://nodejs.org/dist/v{version}/node-v{version}-{os}-{arch}.{ext}'; + const downloadPath = template + .replace(/{version}/g, REMOTE_NODE_VERSION) + .replace(/{os}/g, this.getPlatformName(platform)) + .replace(/{arch}/g, platform.arch) + .replace(/{ext}/g, this.getNodeFileExtension(platform)); + return downloadPath; } - } diff --git a/packages/remote/src/electron-node/setup/remote-setup-service.ts b/packages/remote/src/electron-node/setup/remote-setup-service.ts index 2f78e08ae0665..978a8db66f619 100644 --- a/packages/remote/src/electron-node/setup/remote-setup-service.ts +++ b/packages/remote/src/electron-node/setup/remote-setup-service.ts @@ -23,6 +23,12 @@ import { OS, THEIA_VERSION } from '@theia/core'; import { RemoteNodeSetupService } from './remote-node-setup-service'; import { RemoteSetupScriptService } from './remote-setup-script-service'; +export interface RemoteSetupOptions { + connection: RemoteConnection; + report: RemoteStatusReport; + nodeDownloadTemplate?: string; +} + @injectable() export class RemoteSetupService { @@ -41,7 +47,12 @@ export class RemoteSetupService { @inject(ApplicationPackage) protected readonly applicationPackage: ApplicationPackage; - async setup(connection: RemoteConnection, report: RemoteStatusReport): Promise { + async setup(options: RemoteSetupOptions): Promise { + const { + connection, + report, + nodeDownloadTemplate + } = options; report('Identifying remote system...'); // 1. Identify remote platform const platform = await this.detectRemotePlatform(connection); @@ -57,7 +68,7 @@ export class RemoteSetupService { if (!nodeDirExists) { report('Downloading and installing Node.js on remote...'); // Download the binaries locally and move it via SSH - const nodeArchive = await this.nodeSetupService.downloadNode(platform); + const nodeArchive = await this.nodeSetupService.downloadNode(platform, nodeDownloadTemplate); const remoteNodeZip = this.scriptService.joinPath(platform, applicationDirectory, nodeFileName); await connection.copy(nodeArchive, remoteNodeZip); await this.unzipRemote(connection, platform, remoteNodeZip, applicationDirectory); diff --git a/packages/remote/src/electron-node/ssh/remote-ssh-connection-provider.ts b/packages/remote/src/electron-node/ssh/remote-ssh-connection-provider.ts index 46c177c5db112..3bc1b3bacb5ec 100644 --- a/packages/remote/src/electron-node/ssh/remote-ssh-connection-provider.ts +++ b/packages/remote/src/electron-node/ssh/remote-ssh-connection-provider.ts @@ -20,7 +20,7 @@ import * as fs from '@theia/core/shared/fs-extra'; import SftpClient = require('ssh2-sftp-client'); import { Emitter, Event, MessageService, QuickInputService } from '@theia/core'; import { inject, injectable } from '@theia/core/shared/inversify'; -import { RemoteSSHConnectionProvider } from '../../electron-common/remote-ssh-connection-provider'; +import { RemoteSSHConnectionProvider, RemoteSSHConnectionProviderOptions } from '../../electron-common/remote-ssh-connection-provider'; import { RemoteConnectionService } from '../remote-connection-service'; import { RemoteProxyServerProvider } from '../remote-proxy-server-provider'; import { RemoteConnection, RemoteExecOptions, RemoteExecResult, RemoteExecTester, RemoteStatusReport } from '../remote-types'; @@ -53,15 +53,19 @@ export class RemoteSSHConnectionProviderImpl implements RemoteSSHConnectionProvi protected passwordRetryCount = 3; protected passphraseRetryCount = 3; - async establishConnection(host: string, user: string): Promise { + async establishConnection(options: RemoteSSHConnectionProviderOptions): Promise { const progress = await this.messageService.showProgress({ text: 'Remote SSH' }); const report: RemoteStatusReport = message => progress.report({ message }); report('Connecting to remote system...'); try { - const remote = await this.establishSSHConnection(host, user); - await this.remoteSetup.setup(remote, report); + const remote = await this.establishSSHConnection(options.host, options.user); + await this.remoteSetup.setup({ + connection: remote, + report, + nodeDownloadTemplate: options.nodeDownloadTemplate + }); const registration = this.remoteConnectionService.register(remote); const server = await this.serverProvider.getProxyServer(socket => { remote.forwardOut(socket); @@ -230,11 +234,6 @@ export class RemoteSSHConnectionProviderImpl implements RemoteSSHConnectionProvi callback(END_AUTH); }; } - - isConnectionAlive(remoteId: string): Promise { - return Promise.resolve(Boolean(this.remoteConnectionService.getConnection(remoteId))); - } - } export interface RemoteSSHConnectionOptions { diff --git a/packages/remote/tsconfig.json b/packages/remote/tsconfig.json index b623c1e105ac7..583ad6411ef85 100644 --- a/packages/remote/tsconfig.json +++ b/packages/remote/tsconfig.json @@ -11,6 +11,9 @@ "references": [ { "path": "../core" + }, + { + "path": "../filesystem" } ] } diff --git a/yarn.lock b/yarn.lock index 4b16bb5b92105..7eb5d4bda5d83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8034,11 +8034,6 @@ nanoid@3.3.3: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== -nanoid@3.3.4: - version "3.3.4" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" - integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== - nanoid@^3.3.6: version "3.3.6" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" From 12fc0b434e0ac600a00d24d71c7938f994ef53d4 Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Wed, 25 Oct 2023 13:22:10 +0200 Subject: [PATCH 06/12] Use better naming for dual proxy --- packages/core/src/browser/frontend-application-module.ts | 6 +++--- .../core/src/browser/messaging/ws-connection-provider.ts | 2 +- packages/monaco/src/browser/monaco-frontend-module.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/core/src/browser/frontend-application-module.ts b/packages/core/src/browser/frontend-application-module.ts index 87121d5c91a10..dbfc14fef85f1 100644 --- a/packages/core/src/browser/frontend-application-module.ts +++ b/packages/core/src/browser/frontend-application-module.ts @@ -248,7 +248,7 @@ export const frontendApplicationModule = new ContainerModule((bind, _unbind, _is bind(SelectionService).toSelf().inSingletonScope(); bind(CommandRegistry).toSelf().inSingletonScope().onActivation(({ container }, registry) => { - WebSocketConnectionProvider.createDualProxy(container, commandServicePath, registry); + WebSocketConnectionProvider.createHandler(container, commandServicePath, registry); return registry; }); bind(CommandService).toService(CommandRegistry); @@ -268,7 +268,7 @@ export const frontendApplicationModule = new ContainerModule((bind, _unbind, _is bindMessageService(bind).onActivation(({ container }, messages) => { const client = container.get(MessageClient); - WebSocketConnectionProvider.createDualProxy(container, messageServicePath, client); + WebSocketConnectionProvider.createHandler(container, messageServicePath, client); return messages; }); @@ -296,7 +296,7 @@ export const frontendApplicationModule = new ContainerModule((bind, _unbind, _is bind(QuickAccessContribution).toService(QuickHelpService); bind(QuickPickService).to(QuickPickServiceImpl).inSingletonScope().onActivation(({ container }, quickPickService: QuickPickService) => { - WebSocketConnectionProvider.createDualProxy(container, quickPickServicePath, quickPickService); + WebSocketConnectionProvider.createHandler(container, quickPickServicePath, quickPickService); return quickPickService; }); diff --git a/packages/core/src/browser/messaging/ws-connection-provider.ts b/packages/core/src/browser/messaging/ws-connection-provider.ts index 05e8a25de9928..328cafef13343 100644 --- a/packages/core/src/browser/messaging/ws-connection-provider.ts +++ b/packages/core/src/browser/messaging/ws-connection-provider.ts @@ -54,7 +54,7 @@ export class WebSocketConnectionProvider extends AbstractConnectionProvider(LocalWebSocketConnectionProvider).createProxy(path, arg); } - static createDualProxy(container: interfaces.Container, path: string, arg?: object): void { + static createHandler(container: interfaces.Container, path: string, arg?: object): void { const remote = container.get(WebSocketConnectionProvider); const local = container.get(LocalWebSocketConnectionProvider); remote.createProxy(path, arg); diff --git a/packages/monaco/src/browser/monaco-frontend-module.ts b/packages/monaco/src/browser/monaco-frontend-module.ts index e688a356f8cef..70dff5034bcfc 100644 --- a/packages/monaco/src/browser/monaco-frontend-module.ts +++ b/packages/monaco/src/browser/monaco-frontend-module.ts @@ -160,7 +160,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(MonacoQuickInputImplementation).toSelf().inSingletonScope(); bind(MonacoQuickInputService).toSelf().inSingletonScope().onActivation(({ container }, quickInputService: MonacoQuickInputService) => { - WebSocketConnectionProvider.createDualProxy(container, quickInputServicePath, quickInputService); + WebSocketConnectionProvider.createHandler(container, quickInputServicePath, quickInputService); return quickInputService; }); bind(QuickInputService).toService(MonacoQuickInputService); From 720a06b5306e0186d3a5d66ae38531c216d838a4 Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Wed, 25 Oct 2023 15:20:15 +0200 Subject: [PATCH 07/12] Fix connection disposal on disconnect --- .../electron-node/ssh/remote-ssh-connection-provider.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/remote/src/electron-node/ssh/remote-ssh-connection-provider.ts b/packages/remote/src/electron-node/ssh/remote-ssh-connection-provider.ts index 3bc1b3bacb5ec..2f942de3c9a0f 100644 --- a/packages/remote/src/electron-node/ssh/remote-ssh-connection-provider.ts +++ b/packages/remote/src/electron-node/ssh/remote-ssh-connection-provider.ts @@ -74,13 +74,6 @@ export class RemoteSSHConnectionProviderImpl implements RemoteSSHConnectionProvi server.close(); registration.dispose(); }); - server.on('connection', socket => { - // This event is triggered once the frontend connects to the proxy server. - // When the connection drops, we need to dispose the remote connection - socket.once('close', () => { - remote.dispose(); - }); - }); const localPort = (server.address() as net.AddressInfo).port; remote.localPort = localPort; return localPort.toString(); From 0f8d67805cb73d26ad1d11bfc7ab9af2bcf39b6c Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Wed, 25 Oct 2023 16:59:18 +0200 Subject: [PATCH 08/12] Execute scripts correctly on windows --- .../src/electron-node/setup/remote-setup-script-service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/remote/src/electron-node/setup/remote-setup-script-service.ts b/packages/remote/src/electron-node/setup/remote-setup-script-service.ts index a4d576ed37ae8..b6a7011d59ea3 100644 --- a/packages/remote/src/electron-node/setup/remote-setup-script-service.ts +++ b/packages/remote/src/electron-node/setup/remote-setup-script-service.ts @@ -40,7 +40,7 @@ export class RemoteWindowsScriptStrategy implements RemoteScriptStrategy { } downloadFile(url: string, output: string): string { - return `Invoke-WebRequest -Uri "${url}" -OutFile ${output}`; + return `PowerShell -Command Invoke-WebRequest -Uri "${url}" -OutFile ${output}`; } unzip(file: string, directory: string): string { @@ -48,7 +48,7 @@ export class RemoteWindowsScriptStrategy implements RemoteScriptStrategy { } mkdir(path: string): string { - return `New-Item -Force -itemType Directory -Path "${path}"`; + return `PowerShell -Command New-Item -Force -itemType Directory -Path "${path}"`; } joinPath(...segments: string[]): string { From fe103e43c4eb4949622197c774aa5c55ca58233f Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Wed, 25 Oct 2023 17:10:23 +0200 Subject: [PATCH 09/12] Fix native dependency download location --- .../setup/app-native-dependency-contribution.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/remote/src/electron-node/setup/app-native-dependency-contribution.ts b/packages/remote/src/electron-node/setup/app-native-dependency-contribution.ts index 265bd16635f17..cca2036b20237 100644 --- a/packages/remote/src/electron-node/setup/app-native-dependency-contribution.ts +++ b/packages/remote/src/electron-node/setup/app-native-dependency-contribution.ts @@ -17,6 +17,7 @@ import { injectable } from '@theia/core/shared/inversify'; import { RemoteNativeDependencyContribution, DownloadOptions, DependencyDownload } from './remote-native-dependency-contribution'; import { RemotePlatform } from '../remote-types'; +import { OS } from '@theia/core'; @injectable() export class AppNativeDependencyContribution implements RemoteNativeDependencyContribution { @@ -30,7 +31,15 @@ export class AppNativeDependencyContribution implements RemoteNativeDependencyCo if (remotePlatform.arch !== 'x64') { throw new Error(`Unsupported remote architecture '${remotePlatform.arch}'. Remote support is only available for x64 architectures.`); } - return `${this.appDownloadUrlBase}/v${theiaVersion}/native-dependencies-${remotePlatform.os}-${remotePlatform.arch}.zip`; + let platform: string; + if (remotePlatform.os === OS.Type.Windows) { + platform = 'win32'; + } else if (remotePlatform.os === OS.Type.OSX) { + platform = 'darwin'; + } else { + platform = 'linux'; + } + return `${this.appDownloadUrlBase}/v${theiaVersion}/native-dependencies-${platform}-${remotePlatform.arch}.zip`; } async download(options: DownloadOptions): Promise { From 04ee640cab32994418d02764e62d188f93a5e2da Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Wed, 25 Oct 2023 18:06:53 +0200 Subject: [PATCH 10/12] Fix Windows server startup --- .../src/electron-node/setup/remote-setup-service.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/remote/src/electron-node/setup/remote-setup-service.ts b/packages/remote/src/electron-node/setup/remote-setup-service.ts index 978a8db66f619..16aee1c0ae085 100644 --- a/packages/remote/src/electron-node/setup/remote-setup-service.ts +++ b/packages/remote/src/electron-node/setup/remote-setup-service.ts @@ -89,12 +89,17 @@ export class RemoteSetupService { } protected async startApplication(connection: RemoteConnection, platform: RemotePlatform, remotePath: string, nodeDir: string): Promise { - const nodeExecutable = this.scriptService.joinPath(platform, nodeDir, 'bin', platform.os === OS.Type.Windows ? 'node.exe' : 'node'); + const nodeExecutable = this.scriptService.joinPath(platform, nodeDir, ...(platform.os === OS.Type.Windows ? ['node.exe'] : ['bin', 'node'])); const mainJsFile = this.scriptService.joinPath(platform, remotePath, 'lib', 'backend', 'main.js'); const localAddressRegex = /listening on http:\/\/127.0.0.1:(\d+)/; + let prefix = ''; + if (platform.os === OS.Type.Windows) { + // We might to switch to PowerShell beforehand on Windows + prefix = this.scriptService.exec(platform) + ' '; + } // Change to the remote application path and start a node process with the copied main.js file // This way, our current working directory is set as expected - const result = await connection.execPartial(`cd "${remotePath}";${nodeExecutable}`, + const result = await connection.execPartial(`${prefix}cd "${remotePath}";${nodeExecutable}`, stdout => localAddressRegex.test(stdout), [mainJsFile, '--hostname=127.0.0.1', '--port=0', '--remote']); From cc6270656f9d99dc6d0de6a612cb917a51c7baf3 Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Wed, 25 Oct 2023 21:35:30 +0200 Subject: [PATCH 11/12] Fix missing worker files on windows --- .../remote/src/electron-node/setup/main-copy-contribution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/remote/src/electron-node/setup/main-copy-contribution.ts b/packages/remote/src/electron-node/setup/main-copy-contribution.ts index f917bc678aa8b..e615af69b33cf 100644 --- a/packages/remote/src/electron-node/setup/main-copy-contribution.ts +++ b/packages/remote/src/electron-node/setup/main-copy-contribution.ts @@ -21,7 +21,7 @@ import { RemoteCopyContribution, RemoteCopyRegistry } from './remote-copy-contri export class MainCopyContribution implements RemoteCopyContribution { async copy(registry: RemoteCopyRegistry): Promise { registry.file('package.json'); - await registry.glob('lib/backend/!(native)'); + await registry.glob('lib/backend/**/*.js'); await registry.directory('lib/frontend'); await registry.directory('lib/webview'); } From 82958739ab4bd86346a049413df801ca51632200 Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Thu, 26 Oct 2023 12:03:17 +0200 Subject: [PATCH 12/12] Use correct download location for native deps --- .../setup/app-native-dependency-contribution.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/remote/src/electron-node/setup/app-native-dependency-contribution.ts b/packages/remote/src/electron-node/setup/app-native-dependency-contribution.ts index cca2036b20237..1e0d8e52719cb 100644 --- a/packages/remote/src/electron-node/setup/app-native-dependency-contribution.ts +++ b/packages/remote/src/electron-node/setup/app-native-dependency-contribution.ts @@ -22,10 +22,7 @@ import { OS } from '@theia/core'; @injectable() export class AppNativeDependencyContribution implements RemoteNativeDependencyContribution { - // TODO: Points for testing purposes to a non-theia repo - // Should be replaced with: - // 'https://github.com/eclipse-theia/theia/releases/download' - appDownloadUrlBase = 'https://github.com/msujew/theia/releases/download'; + appDownloadUrlBase = 'https://github.com/eclipse-theia/theia/releases/download'; protected getDefaultURLForFile(remotePlatform: RemotePlatform, theiaVersion: string): string { if (remotePlatform.arch !== 'x64') {