Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: infer second terminal cwd from the first one #2852

Merged
merged 6 commits into from
Jul 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/terminal-next/__tests__/browser/mock.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ export class MockSocketService implements ITerminalService {
return OperatingSystem.Linux;
}

async getCwd() {
return undefined;
}

private _handleStdoutMessage(sessionId: string, handler: (json: any) => void) {
const socket = this._socks.get(sessionId);
if (!socket) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ describe('TerminalServiceClientImpl', () => {

expect(typeof terminalServiceClient.getProcessId(mockId)).toEqual('number');
expect(typeof terminalServiceClient.getShellName(mockId)).toEqual('string');
expect(await terminalServiceClient.getCwd(mockId)).toBeTruthy();
expect(receiveData.indexOf('message test') > -1).toEqual(true);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export class TerminalCommandContribution implements CommandContribution {
},
);

// 分屏
// 拆分终端
registry.registerCommand(
{
...TERMINAL_COMMANDS.SPLIT,
Expand Down
11 changes: 11 additions & 0 deletions packages/terminal-next/src/browser/terminal.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,17 @@ export class TerminalClient extends Disposable implements ITerminalClient {
config: defaultProfile,
};
}
if (!options.cwd) {
// resolve cwd from the group first widget
const group = widget.group;
if (group.widgets.length > 1 && group.widgets[0]) {
const cwd = await this.internalService.getCwd(group.widgets[0].id);
if (cwd) {
options.cwd = cwd;
}
}
}

await this._checkWorkspace();

const cwd = options.cwd ?? (options?.config as IShellLaunchConfig)?.cwd ?? this._workspacePath;
Expand Down
4 changes: 2 additions & 2 deletions packages/terminal-next/src/browser/terminal.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,8 @@ export class TerminalController extends WithEventBus implements ITerminalControl
}

private async _createClient(widget: IWidget, options?: ICreateTerminalOptions) {
const client = await this.clientFactory(widget, /** @type ICreateTerminalOptions */ options);
this.logger.log('create client with clientFactory2', client);
const client = await this.clientFactory(widget, options);
this.logger.log('create client with ITerminalClientFactory2', client);
return this.setupClient(widget, client);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,7 @@ export class TerminalInternalService implements ITerminalInternalService {
}
return await this.service.attachByLaunchConfig(sessionId, cols, rows, launchConfig, xterm);
}
async getCwd(sessionId: string): Promise<string | undefined> {
return await this.service.getCwd(sessionId);
}
}
8 changes: 8 additions & 0 deletions packages/terminal-next/src/browser/terminal.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,14 @@ export class NodePtyTerminalService implements ITerminalService {
});
}

async getCwd(sessionId: string) {
try {
return await this.serviceClientRPC.getCwd(sessionId);
} catch {
return undefined;
}
}

async getDefaultSystemShell(): Promise<string> {
return await this.serviceClientRPC.getDefaultSystemShell(await this.getOS());
}
Expand Down
18 changes: 10 additions & 8 deletions packages/terminal-next/src/common/pty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,16 @@ export interface IPtySpawnOptions {
ptyLineCacheSize?: number;
}

export interface IPtyProcess extends INodePty {
export interface IPtyProcessProxy extends INodePty {
/**
* @deprecated 请使用 `IPty.launchConfig` 的 shellPath 字段
*/
bin: string;
launchConfig: IShellLaunchConfig;
parsedName: string;
}

export interface IPtyProcessProxy extends IPtyProcess {
getProcessDynamically(): MaybePromise<string>;
getCwd(): Promise<string | undefined>;
}

export const ITerminalServicePath = 'ITerminalServicePath';
Expand Down Expand Up @@ -127,6 +126,11 @@ export interface IPtyProxyRPCService {
*/
$getProcess(pid: number): string;

/**
* Get the current working directory for the given process ID.
*/
$getCwd(pid: number): Promise<string | undefined>;

/**
* 检查Session对应的进程是否存活
* @param sessionId 终端的sessionId
Expand Down Expand Up @@ -290,14 +294,11 @@ export interface TerminalOptions {

export const ITerminalNodeService = Symbol('ITerminalNodeService');
export interface ITerminalNodeService {
/**
* @deprecated this overload signature will be removed in 2.17.0
*/
create2(id: string, launchConfig: IShellLaunchConfig): Promise<IPtyProcess | undefined>;
create2(id: string, cols: number, rows: number, options: IShellLaunchConfig): Promise<IPtyProcess | undefined>;
create2(id: string, cols: number, rows: number, options: IShellLaunchConfig): Promise<IPtyProcessProxy | undefined>;
onMessage(id: string, msg: string): void;
resize(id: string, rows: number, cols: number): void;
getShellName(id: string): string;
getCwd(id: string): Promise<string | undefined>;
getProcessId(id: string): number;
disposeById(id: string): void;
dispose(): void;
Expand Down Expand Up @@ -355,6 +356,7 @@ export interface ITerminalServiceClient {
getDefaultSystemShell(os: OperatingSystem): Promise<string>;
getOS(): OperatingSystem;
getCodePlatformKey(): Promise<'osx' | 'windows' | 'linux'>;
getCwd(id: string): Promise<string | undefined>;
}

export interface ITerminalInfo {
Expand Down
1 change: 1 addition & 0 deletions packages/terminal-next/src/common/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export interface ITerminalService {
getProfiles(autoDetect: boolean): Promise<ITerminalProfile[]>;
getDefaultSystemShell(): Promise<string>;
getCodePlatformKey(): Promise<'osx' | 'windows' | 'linux'>;
getCwd(sessionId: string): Promise<string | undefined>;
}

export const ITerminalInternalService = Symbol('ITerminalInternalService');
Expand Down
13 changes: 11 additions & 2 deletions packages/terminal-next/src/node/pty.manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ export interface IPtyServiceManager {
pause(pid: number): void;
resume(pid: number): void;
kill(pid: number, signal?: string): void;
getProcess(pid): Promise<string>;
getProcess(pid: number): Promise<string>;
getCwd(pid: number): Promise<string | undefined>;
checkSession(sessionId: string): Promise<boolean>;
}

Expand Down Expand Up @@ -104,10 +105,14 @@ export class PtyServiceManager implements IPtyServiceManager {
return new PtyProcessProxy(ptyRemoteProxy, this);
}

async getProcess(pid: any): Promise<string> {
async getProcess(pid: number): Promise<string> {
return await this.ptyServiceProxy.$getProcess(pid);
}

async getCwd(pid: number): Promise<string | undefined> {
return await this.ptyServiceProxy.$getCwd(pid);
}

// 实现 IPty 的需要回调的逻辑接口,同时注入
onData(pid: number, listener: (e: string) => any): pty.IDisposable {
const monitorListener = (resString) => {
Expand Down Expand Up @@ -208,6 +213,10 @@ class PtyProcessProxy implements IPtyProcessProxy {
return process;
}

async getCwd(): Promise<string | undefined> {
return await this.ptyServiceManager.getCwd(this.pid);
}

onData: pty.IEvent<string>;
onExit: pty.IEvent<{ exitCode: number; signal?: number }>;

Expand Down
2 changes: 1 addition & 1 deletion packages/terminal-next/src/node/pty.proxy.remote.exec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PtyServiceProxyRPCProvider } from './pty.proxy';

// 双容器模式下,需要以本文件作为entry单独打包出一个可执行文件,运行在DEV容器中
// 双容器模式下,需要以本文件作为 entry 单独打包出一个可执行文件,运行在 DEV 容器中
const proxyProvider = new PtyServiceProxyRPCProvider();
proxyProvider.initServer();
25 changes: 24 additions & 1 deletion packages/terminal-next/src/node/pty.proxy.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
/* eslint-disable no-console */
import childProcess from 'child_process';
import fs from 'fs';
import net, { ListenOptions, Server } from 'net';
import os from 'os';
import { promisify } from 'util';

import * as pty from 'node-pty';

import { RPCServiceCenter, initRPCService } from '@opensumi/ide-connection';
import { createSocketConnection } from '@opensumi/ide-connection/lib/node';
import { DisposableCollection, getDebugLogger } from '@opensumi/ide-core-node';
import { isMacintosh, isLinux } from '@opensumi/ide-utils/lib/platform';

import {
IPtyProxyRPCService,
Expand All @@ -17,6 +20,9 @@ import {
TERMINAL_ID_SEPARATOR,
} from '../common';

const readLinkAsync = promisify(fs.readlink);
const execAsync = promisify(childProcess.exec);

const PTY_LINE_DATA_CACHE_DEFAULT_SIZE = 500;

// 存储Pty-onData返回的数据行,用于用户Resume场景下的数据恢复
Expand Down Expand Up @@ -235,6 +241,10 @@ export class PtyServiceProxy implements IPtyProxyRPCService {
const ptyInstance = this.ptyInstanceMap.get(pid);
return ptyInstance?.process || '';
}

$getCwd(pid: number): Promise<string | undefined> {
return getPidCwd(pid);
}
}

// 需要单独运行PtyServer的时候集成此Class然后运行initServer
Expand Down Expand Up @@ -305,3 +315,16 @@ export class PtyServiceProxyRPCProvider {
return this.ptyServiceProxy;
}
}

async function getPidCwd(pid: number): Promise<string | undefined> {
if (isLinux) {
return await readLinkAsync(`/proc/${pid}/cwd`);
}
if (isMacintosh) {
const result = await execAsync(`lsof -a -d cwd -p ${pid} | tail -1 | awk '{print $9}'`);
if (result.stdout) {
return result.stdout.trim();
}
}
return undefined;
}
19 changes: 13 additions & 6 deletions packages/terminal-next/src/node/pty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { getShellPath } from '@opensumi/ide-core-node/lib/bootstrap/shell-path';

import { IShellLaunchConfig, ITerminalLaunchError } from '../common';
import { IProcessReadyEvent, IProcessExitEvent } from '../common/process';
import { IPtyProcess, IPtySpawnOptions } from '../common/pty';
import { IPtyProcessProxy, IPtySpawnOptions } from '../common/pty';

import { IPtyServiceManager, PtyServiceManagerToken } from './pty.manager';
import { findExecutable } from './shell';
Expand All @@ -34,7 +34,7 @@ export class PtyService extends Disposable {
protected readonly ptyServiceManager: IPtyServiceManager;

protected readonly _ptyOptions: pty.IPtyForkOptions | pty.IWindowsPtyForkOptions;
private _ptyProcess: IPtyProcess | undefined;
private _ptyProcess: IPtyProcessProxy | undefined;

private readonly _onData = new Emitter<string>();
readonly onData = this._onData.event;
Expand Down Expand Up @@ -231,13 +231,13 @@ export class PtyService extends Disposable {
}),
);

(ptyProcess as IPtyProcess).bin = options.executable as string;
(ptyProcess as IPtyProcess).launchConfig = options;
(ptyProcess as IPtyProcess).parsedName = path.basename(options.executable as string);
ptyProcess.bin = options.executable as string;
ptyProcess.launchConfig = options;
ptyProcess.parsedName = path.basename(options.executable as string);

this._sendProcessId(ptyProcess.pid);

this._ptyProcess = ptyProcess as IPtyProcess;
this._ptyProcess = ptyProcess;
}
private _sendProcessId(pid: number) {
this._onReady.fire({
Expand All @@ -262,6 +262,13 @@ export class PtyService extends Disposable {
return this._ptyProcess?.pid || -1;
}

async getCwd() {
if (!this._ptyProcess) {
return undefined;
}
return this._ptyProcess.getCwd();
}

resize(rows: number, cols: number) {
try {
this._ptyProcess?.resize(cols, rows);
Expand Down
2 changes: 1 addition & 1 deletion packages/terminal-next/src/node/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ let _TERMINAL_DEFAULT_SHELL_WINDOWS: string | null = null;
async function getSystemShellWindows(env = process.env): Promise<string> {
if (!_TERMINAL_DEFAULT_SHELL_WINDOWS) {
const isAtLeastWindows10 = isWindows && parseFloat(release()) >= 10;
const is32ProcessOn64Windows = env.hasOwnProperty('PROCESSOR_ARCHITEW6432');
const is32ProcessOn64Windows = Object.prototype.hasOwnProperty.call(env, 'PROCESSOR_ARCHITEW6432');
const powerShellPath = `${env['windir']}\\${
is32ProcessOn64Windows ? 'Sysnative' : 'System32'
}\\WindowsPowerShell\\v1.0\\powershell.exe`;
Expand Down
8 changes: 6 additions & 2 deletions packages/terminal-next/src/node/terminal.service.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
ITerminalError,
} from '../common';
import { IDetectProfileOptions, ITerminalProfile } from '../common/profile';
import { IPtyProcess } from '../common/pty';
import { IPtyProcessProxy } from '../common/pty';
import { WindowsShellType, WINDOWS_DEFAULT_SHELL_PATH_MAPS } from '../common/shell';

import { findShellExecutableAsync, getSystemShell, WINDOWS_GIT_BASH_PATHS } from './shell';
Expand All @@ -31,7 +31,7 @@ interface IRPCTerminalService {
*/
@Injectable()
export class TerminalServiceClientImpl extends RPCService<IRPCTerminalService> implements ITerminalServiceClient {
private terminalMap: Map<string, IPtyProcess> = new Map();
private terminalMap: Map<string, IPtyProcessProxy> = new Map();

@Autowired(ITerminalNodeService)
private terminalService: ITerminalNodeService;
Expand Down Expand Up @@ -187,4 +187,8 @@ export class TerminalServiceClientImpl extends RPCService<IRPCTerminalService> i
dispose() {
this.terminalService.closeClient(this.clientId);
}

getCwd(id: string): Promise<string | undefined> {
return this.terminalService.getCwd(id);
}
}
39 changes: 13 additions & 26 deletions packages/terminal-next/src/node/terminal.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Injectable, Autowired, INJECTOR_TOKEN, Injector } from '@opensumi/di';
import { INodeLogger, AppConfig, isDevelopment, isElectronNode } from '@opensumi/ide-core-node';

import { ETerminalErrorType, ITerminalNodeService, ITerminalServiceClient, TERMINAL_ID_SEPARATOR } from '../common';
import { IPtyProcess, IShellLaunchConfig } from '../common/pty';
import { IPtyProcessProxy, IShellLaunchConfig } from '../common/pty';

import { PtyService } from './pty';
import { IPtyServiceManager, PtyServiceManagerToken } from './pty.manager';
Expand All @@ -13,10 +13,6 @@ const BATCH_MAX_SIZE = 200 * 1024;
// 批处理延时
const BATCH_DURATION_MS = 16;

/**
* terminal service 的具体实现
* @lengthmin: 其实这里应该换成每个实例持有一个 pty 实例,待讨论并推进实现
*/
@Injectable()
export class TerminalServiceImpl implements ITerminalNodeService {
static TerminalPtyCloseThreshold = 10 * 1000;
Expand Down Expand Up @@ -125,32 +121,14 @@ export class TerminalServiceImpl implements ITerminalNodeService {
serviceClient.clientMessage(sessionId, ptyData);
}

public async create2(id: string, cols: IShellLaunchConfig): Promise<IPtyProcess | undefined>;
public async create2(
id: string,
sessionId: string,
cols: number,
rows: number,
options: IShellLaunchConfig,
): Promise<IPtyProcess | undefined>;
public async create2(
sessionId: string,
_cols: unknown,
_rows?: unknown,
_launchConfig?: unknown,
): Promise<IPtyProcess | undefined> {
launchConfig: IShellLaunchConfig,
): Promise<IPtyProcessProxy | undefined> {
const clientId = sessionId.split(TERMINAL_ID_SEPARATOR)[0];
let ptyService: PtyService | undefined;
let cols = _cols as number;
let rows = _rows as number;
let launchConfig = _launchConfig as IShellLaunchConfig;
if (!(typeof cols === 'number')) {
launchConfig = cols as IShellLaunchConfig;
cols = (launchConfig as any).cols;
rows = (launchConfig as any).rows;
if ((launchConfig as any).shellPath) {
launchConfig.executable = (launchConfig as any).shellPath;
}
}

try {
ptyService = this.injector.get(PtyService, [sessionId, launchConfig, cols, rows]);
Expand Down Expand Up @@ -291,4 +269,13 @@ export class TerminalServiceImpl implements ITerminalNodeService {
private getTerminal(id: string) {
return this.terminalProcessMap.get(id);
}

async getCwd(id: string): Promise<string | undefined> {
const ptyService = this.getTerminal(id);
if (!ptyService) {
return undefined;
}

return await ptyService.getCwd();
}
}