Skip to content

Commit

Permalink
Support remote port forwarding (#13439)
Browse files Browse the repository at this point in the history
* basics for dev-container support

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* basic creating and connecting to container working

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* open workspace when opening container

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* save and reuse last USed container per workspace

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* restart container if running

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* better container creation extension features

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* added dockerfile support

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* rebuild container if devcontainer.json has been changed since last use

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* fix build

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* fixed checking if container needs rebuild

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* working port forwarding via exec instance

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* review changes

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* fix import

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* smaller fixes and added support for multiple devcontainer configuration files

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* basic output window for devcontainer build

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* smaller review changes and nicer dockerfile.json detection code

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* fixed build and docuemented implemented devcontainer.json properties

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* Fix unneeded URI conversion (#13415)

* Fix quickpick problems found in IDE testing (#13451)

Fixes #13450, #13449

contributed on behalf of STMicroelectronics

Signed-off-by: Thomas Mäder <t.s.maeder@gmail.com>

* Fix rending of quickpick buttons (#13342)

Ensure that  the Theia specific wrapper for the MonacoQuickPickItem properly forwards assignments of the "buttons" property to the wrapped item.

Fixes #13076

Contributed on behalf of STMicroelectronics

* electron: allow accessing the metrics endpoint for performance analysis (#13380)

By default, when running Theia in Electron, all endpoints are protected
by the ElectronTokenValidator.
This patch allows accessing the '/metrics' endpoint without a token,
which enables us to collect metrics for performance analysis.

For this, ElectronTokenValidator is extended to allow access to the
metrics endpoint. All other endpoints are still protected.

Contributed on behalf of STMicroelectronics

Signed-off-by: Olaf Lessenich <olessenich@eclipsesource.com>

* fixed renaming and moving of open notebooks (#13467)

* fixed renameing of open notebooks

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* fixed moving of notebook editors to other areas

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

---------

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* [playwright] Update documentation

Since a recent enhancement/refactoring of @theia/playwright, to permit using it in Theia
Electron applications, the way to load an application has changed. This commit is an
attempt to update the examples that are part of the documentation. I validated the changes
in the "theia-playwright-template" repository, and so I have adapted the sample code to
that repo's linting rules (using single quotes instead of double).

It's possible that other things have changed, that I have not yet encountered, but this
should be a good step forward, at least for those just getting started integrating
playwright to test their Theia-based app.

Signed-off-by: Marc Dumais <marc.dumais@ericsson.com>

* basics for dev-container support

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* basic creating and connecting to container working

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* added dockerfile support

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* added port forwarding inlcuding ui

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* basic port/address validation

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* fixed allready forwarded port checking

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* rebase  fixes

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* removed unused file

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* review changes

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* fixed widget focus and message margin

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* default port binding now shows as 0.0.0.0

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

---------

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>
Signed-off-by: Thomas Mäder <t.s.maeder@gmail.com>
Signed-off-by: Olaf Lessenich <olessenich@eclipsesource.com>
Signed-off-by: Marc Dumais <marc.dumais@ericsson.com>
Co-authored-by: Alexander Taran <Alexander-Taran@users.noreply.github.com>
Co-authored-by: Thomas Mäder <tsmaeder@users.noreply.github.com>
Co-authored-by: Tobias Ortmayr <tortmayr@eclipsesource.com>
Co-authored-by: Olaf Lessenich <olessenich@eclipsesource.com>
Co-authored-by: Marc Dumais <marc.dumais@ericsson.com>
  • Loading branch information
6 people authored Mar 22, 2024
1 parent c2b0704 commit 1a03381
Show file tree
Hide file tree
Showing 11 changed files with 420 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,13 @@ interface ContainerTerminalSession {
executeCommand(cmd: string, args?: string[]): Promise<{ stdout: string, stderr: string }>;
}

interface ContainerTerminalSession {
execution: Docker.Exec,
stdout: WriteStream,
stderr: WriteStream,
executeCommand(cmd: string, args?: string[]): Promise<{ stdout: string, stderr: string }>;
}

export class RemoteDockerContainerConnection implements RemoteConnection {

id: string;
Expand Down Expand Up @@ -179,12 +186,12 @@ export class RemoteDockerContainerConnection implements RemoteConnection {
this.container = options.container;
}

async forwardOut(socket: Socket): Promise<void> {
async forwardOut(socket: Socket, port?: number): Promise<void> {
const node = `${this.remoteSetupResult.nodeDirectory}/bin/node`;
const devContainerServer = `${this.remoteSetupResult.applicationDirectory}/backend/dev-container-server.js`;
try {
const ttySession = await this.container.exec({
Cmd: ['sh', '-c', `${node} ${devContainerServer} -target-port=${this.remotePort}`],
Cmd: ['sh', '-c', `${node} ${devContainerServer} -target-port=${port ?? this.remotePort}`],
AttachStdin: true, AttachStdout: true, AttachStderr: true
});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// *****************************************************************************
// Copyright (C) 2024 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 { nls } from '@theia/core';
import { AbstractViewContribution } from '@theia/core/lib/browser';
import { injectable } from '@theia/core/shared/inversify';
import { PortForwardingWidget, PORT_FORWARDING_WIDGET_ID } from './port-forwarding-widget';

@injectable()
export class PortForwardingContribution extends AbstractViewContribution<PortForwardingWidget> {
constructor() {
super({
widgetId: PORT_FORWARDING_WIDGET_ID,
widgetName: nls.localizeByDefault('Ports'),
defaultWidgetOptions: {
area: 'bottom'
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// *****************************************************************************
// Copyright (C) 2024 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 { Emitter } from '@theia/core';
import { inject, injectable } from '@theia/core/shared/inversify';
import { RemotePortForwardingProvider } from '../../electron-common/remote-port-forwarding-provider';

export interface ForwardedPort {
localPort?: number;
address?: string;
origin?: string;
editing: boolean;
}

@injectable()
export class PortForwardingService {

@inject(RemotePortForwardingProvider)
readonly provider: RemotePortForwardingProvider;

protected readonly onDidChangePortsEmitter = new Emitter<void>();
readonly onDidChangePorts = this.onDidChangePortsEmitter.event;

forwardedPorts: ForwardedPort[] = [];

forwardNewPort(origin?: string): ForwardedPort {
const index = this.forwardedPorts.push({ editing: true, origin });
return this.forwardedPorts[index - 1];
}

updatePort(port: ForwardedPort, newAdress: string): void {
const connectionPort = new URLSearchParams(location.search).get('port');
if (!connectionPort) {
// if there is no open remote connection we can't forward a port
return;
}

const parts = newAdress.split(':');
if (parts.length === 2) {
port.address = parts[0];
port.localPort = parseInt(parts[1]);
} else {
port.localPort = parseInt(parts[0]);
}

port.editing = false;

this.provider.forwardPort(parseInt(connectionPort), { port: port.localPort!, address: port.address });
this.onDidChangePortsEmitter.fire();
}

removePort(port: ForwardedPort): void {
const index = this.forwardedPorts.indexOf(port);
if (index !== -1) {
this.forwardedPorts.splice(index, 1);
this.provider.portRemoved({ port: port.localPort! });
this.onDidChangePortsEmitter.fire();
}
}

isValidAddress(address: string): boolean {
const match = address.match(/^(.*:)?\d+$/);
if (!match) {
return false;
}

const port = parseInt(address.includes(':') ? address.split(':')[1] : address);

return !this.forwardedPorts.some(p => p.localPort === port);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// *****************************************************************************
// Copyright (C) 2024 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 React from '@theia/core/shared/react';
import { ReactNode } from '@theia/core/shared/react';
import { OpenerService, ReactWidget } from '@theia/core/lib/browser';
import { nls, URI } from '@theia/core';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { ForwardedPort, PortForwardingService } from './port-forwarding-service';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';

export const PORT_FORWARDING_WIDGET_ID = 'port-forwarding-widget';

@injectable()
export class PortForwardingWidget extends ReactWidget {

@inject(PortForwardingService)
protected readonly portForwardingService: PortForwardingService;

@inject(OpenerService)
protected readonly openerService: OpenerService;

@inject(ClipboardService)
protected readonly clipboardService: ClipboardService;

@postConstruct()
protected init(): void {
this.id = PORT_FORWARDING_WIDGET_ID;
this.node.tabIndex = -1;
this.title.label = nls.localizeByDefault('Ports');
this.title.caption = this.title.label;
this.title.closable = true;
this.update();

this.portForwardingService.onDidChangePorts(() => this.update());
}

protected render(): ReactNode {
if (this.portForwardingService.forwardedPorts.length === 0) {
return <div>
<p style={{ marginLeft: 'calc(var(--theia-ui-padding) * 2)' }}>
{nls.localizeByDefault('No forwarded ports. Forward a port to access your locally running services over the internet.\n[Forward a Port]({0})').split('\n')[0]}
</p>
{this.renderForwardPortButton()}
</div>;
}

return <div>
<table className='port-table'>
<thead>
<tr>
<th className='port-table-header'>{nls.localizeByDefault('Port')}</th>
<th className='port-table-header'>{nls.localizeByDefault('Address')}</th>
<th className='port-table-header'>{nls.localizeByDefault('Running Process')}</th>
<th className='port-table-header'>{nls.localizeByDefault('Origin')}</th>
</tr>
</thead>
<tbody>
{this.portForwardingService.forwardedPorts.map(port => (
<tr key={port.localPort ?? 'editing'}>
{this.renderPortColumn(port)}
{this.renderAddressColumn(port)}
<td></td>
<td>{port.origin ? nls.localizeByDefault(port.origin) : ''}</td>
</tr>
))}
{!this.portForwardingService.forwardedPorts.some(port => port.editing) && <tr><td>{this.renderForwardPortButton()}</td></tr>}
</tbody>
</table>
</div>;
}

protected renderForwardPortButton(): ReactNode {
return <button className='theia-button' onClick={() => {
this.portForwardingService.forwardNewPort('User Forwarded');
this.update();
}
}>{nls.localizeByDefault('Forward a Port')}</button>;
}

protected renderAddressColumn(port: ForwardedPort): ReactNode {
const address = `${port.address ?? '0.0.0.0'}:${port.localPort}`;
return <td>
<div className='button-cell'>
<span style={{ flexGrow: 1 }} className='forwarded-address' onClick={async e => {
if (e.ctrlKey) {
const uri = new URI(`http://${address}`);
(await this.openerService.getOpener(uri)).open(uri);
}
}} title={nls.localizeByDefault('Follow link') + ' (ctrl/cmd + click)'}>
{port.localPort ? address : ''}
</span>
{
port.localPort &&
<span className='codicon codicon-clippy action-label' title={nls.localizeByDefault('Copy Local Address')} onClick={() => {
this.clipboardService.writeText(address);
}}></span>
}
</div>
</td>;
}

protected renderPortColumn(port: ForwardedPort): ReactNode {
return port.editing ?
<td><PortEditingInput port={port} service={this.portForwardingService} /></td> :
<td>
<div className='button-cell'>
<span style={{ flexGrow: 1 }}>{port.localPort}</span>
<span className='codicon codicon-close action-label' title={nls.localizeByDefault('Stop Forwarding Port')} onClick={() => {
this.portForwardingService.removePort(port);
this.update();
}}></span>
</div>
</td>;
}

}

function PortEditingInput({ port, service }: { port: ForwardedPort, service: PortForwardingService }): React.JSX.Element {
const [error, setError] = React.useState(false);
return <input className={`theia-input forward-port-button${error ? ' port-edit-input-error' : ''}`} port-edit-input-error={error}
autoFocus defaultValue={port.address ? `${port.address}:${port.localPort}` : port.localPort ?? ''}
placeholder={nls.localizeByDefault('Port number or address (eg. 3000 or 10.10.10.10:2000).')}
onKeyDown={e => e.key === 'Enter' && !error && service.updatePort(port, e.currentTarget.value)}
onKeyUp={e => setError(!service.isValidAddress(e.currentTarget.value))}></input>;

}
25 changes: 22 additions & 3 deletions packages/remote/src/electron-browser/remote-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import { bindContributionProvider, CommandContribution } from '@theia/core';
import { ContainerModule } from '@theia/core/shared/inversify';
import { FrontendApplicationContribution, WebSocketConnectionProvider } from '@theia/core/lib/browser';
import { bindViewContribution, FrontendApplicationContribution, WidgetFactory } 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';
Expand All @@ -26,6 +26,12 @@ import { RemoteStatusService, RemoteStatusServicePath } from '../electron-common
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';
import { PortForwardingWidget, PORT_FORWARDING_WIDGET_ID } from './port-forwarding/port-forwarding-widget';
import { PortForwardingContribution } from './port-forwarding/port-forwading-contribution';
import { PortForwardingService } from './port-forwarding/port-forwarding-service';
import { RemotePortForwardingProvider, RemoteRemotePortForwardingProviderPath } from '../electron-common/remote-port-forwarding-provider';
import { ServiceConnectionProvider } from '@theia/core/lib/browser/messaging/service-connection-provider';
import '../../src/electron-browser/style/port-forwarding-widget.css';

export default new ContainerModule((bind, _, __, rebind) => {
bind(RemoteFrontendContribution).toSelf().inSingletonScope();
Expand All @@ -42,8 +48,21 @@ export default new ContainerModule((bind, _, __, rebind) => {

bind(RemoteService).toSelf().inSingletonScope();

bind(PortForwardingWidget).toSelf();
bind(WidgetFactory).toDynamicValue(context => ({
id: PORT_FORWARDING_WIDGET_ID,
createWidget: () => context.container.get<PortForwardingWidget>(PortForwardingWidget)
}));

bindViewContribution(bind, PortForwardingContribution);
bind(PortForwardingService).toSelf().inSingletonScope();

bind(RemoteSSHConnectionProvider).toDynamicValue(ctx =>
WebSocketConnectionProvider.createLocalProxy<RemoteSSHConnectionProvider>(ctx.container, RemoteSSHConnectionProviderPath)).inSingletonScope();
ServiceConnectionProvider.createLocalProxy<RemoteSSHConnectionProvider>(ctx.container, RemoteSSHConnectionProviderPath)).inSingletonScope();
bind(RemoteStatusService).toDynamicValue(ctx =>
WebSocketConnectionProvider.createLocalProxy<RemoteStatusService>(ctx.container, RemoteStatusServicePath)).inSingletonScope();
ServiceConnectionProvider.createLocalProxy<RemoteStatusService>(ctx.container, RemoteStatusServicePath)).inSingletonScope();

bind(RemotePortForwardingProvider).toDynamicValue(ctx =>
ServiceConnectionProvider.createLocalProxy<RemotePortForwardingProvider>(ctx.container, RemoteRemotePortForwardingProviderPath)).inSingletonScope();

});
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/********************************************************************************
* Copyright (C) 2024 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
********************************************************************************/

.port-table {
width: 100%;
margin: calc(var(--theia-ui-padding) * 2);
table-layout: fixed;
}

.port-table-header {
text-align: left;
}

.forward-port-button {
margin-left: 0;
width: 100%;
}

.button-cell {
display: flex;
padding-right: var(--theia-ui-padding);
}

.forwarded-address:hover {
cursor: pointer;
text-decoration: underline;
}

.port-edit-input-error {
outline-color: var(--theia-inputValidation-errorBorder);
}
Loading

0 comments on commit 1a03381

Please sign in to comment.