diff --git a/packages/extension/__tests__/hosted/api/vscode/ext.host.task.test.ts b/packages/extension/__tests__/hosted/api/vscode/ext.host.task.test.ts index 93967d08a6..d1536b70e9 100644 --- a/packages/extension/__tests__/hosted/api/vscode/ext.host.task.test.ts +++ b/packages/extension/__tests__/hosted/api/vscode/ext.host.task.test.ts @@ -24,7 +24,6 @@ import { TerminalTaskSystem } from '@opensumi/ide-task/lib/browser/terminal-task import { ITaskService, ITaskSystem } from '@opensumi/ide-task/lib/common'; import { ITerminalApiService, - ITerminalClientFactory, ITerminalController, ITerminalGroupViewService, ITerminalInternalService, @@ -148,13 +147,6 @@ describe('ExtHostTask API', () => { token: ITaskSystem, useClass: TerminalTaskSystem, }, - { - token: ITerminalClientFactory, - useFactory: - (injector) => - (widget, options = {}) => - TerminalClientFactory.createClient(injector, widget, options), - }, { token: IVariableResolverService, useClass: VariableResolverService, diff --git a/packages/extension/__tests__/node/extension.host.manager.common-tester.ts b/packages/extension/__tests__/node/extension.host.manager.common-tester.ts index 28fb32db04..43de44ed71 100644 --- a/packages/extension/__tests__/node/extension.host.manager.common-tester.ts +++ b/packages/extension/__tests__/node/extension.host.manager.common-tester.ts @@ -83,17 +83,17 @@ export const extensionHostManagerTester = (options: IExtensionHostManagerTesterO }); it('tree kill', async () => { expect.assertions(2); - const defered = new Deferred(); + const deferred = new Deferred(); const pid = await extensionHostManager.fork(extHostPath); extensionHostManager.onExit(pid, async (code, signal) => { expect(signal).toBe('SIGTERM'); // tree-kill 使用 process.kill 不能使用 killed 判断 expect(await extensionHostManager.isRunning(pid)).toBeFalsy(); - defered.resolve(); + deferred.resolve(); }); await extensionHostManager.treeKill(pid); - await defered.promise; + await deferred.promise; }); it('findDebugPort', async () => { @@ -104,14 +104,14 @@ export const extensionHostManagerTester = (options: IExtensionHostManagerTesterO it('on output', async () => { expect.assertions(1); - const defered = new Deferred(); + const deferred = new Deferred(); const pid = await extensionHostManager.fork(extHostPath, [], { silent: true }); extensionHostManager.onOutput(pid, (output) => { expect(output.data).toContain('send ready'); - defered.resolve(); + deferred.resolve(); }); - await defered.promise; + await deferred.promise; }); it('dispose process', async () => { const pid = await extensionHostManager.fork(extHostPath); diff --git a/packages/i18n/src/common/en-US.lang.ts b/packages/i18n/src/common/en-US.lang.ts index 8c9ee24b84..6b198d069c 100644 --- a/packages/i18n/src/common/en-US.lang.ts +++ b/packages/i18n/src/common/en-US.lang.ts @@ -628,7 +628,7 @@ export const localizationBundle = { 'terminal.new': 'Create Terminal', 'terminal.split': 'Split Terminal', 'terminal.clear': 'Remove All Terminals', - 'terminal.clear.content': 'Clear All Terminals', + 'terminal.clear.content': 'Clear All Contents', 'terminal.independ': 'Independent Terminal', 'terminal.maximum': 'Maximum Terminal Panel', 'terminal.or': 'Or', @@ -648,7 +648,7 @@ export const localizationBundle = { 'terminal.menu.clearGroups': 'Clear All Terminals', 'terminal.menu.selectType': 'Default Terminal Type', 'terminal.menu.moreSettings': 'More Settings', - 'terminal.menu.clearCurrentContent': 'Clear All', + 'terminal.menu.clearCurrentContent': 'Clear', 'terminal.menu.selectCurrentContent': 'Select All', 'terminal.menu.clearAllContents': 'Clear All Terminals Content', 'terminal.menu.selectAllContent': 'Select All Terminals Content', @@ -873,6 +873,7 @@ export const localizationBundle = { 'TaskService.pickRunTask': 'Select the task to run', 'TerminalTaskSystem.terminalName': 'Task - {0}', 'terminal.integrated.exitedWithCode': 'The terminal process terminated with exit code: {0}', + // reuseTerminal: "Terminal will be reused by tasks, press 'r' to rerun task. press any other key to close it.", reuseTerminal: 'Terminal will be reused by tasks, press any key to close it.', 'toolbar-customize.buttonDisplay.description': 'Button Style', @@ -973,7 +974,16 @@ export const localizationBundle = { 'debug.terminal.label': 'Javascript Debug Terminal', 'output.channel.clear': 'Clear Output Panel', - 'command.runTask': 'Run Task', + + 'workbench.action.tasks.runTask': 'Run Task', + 'workbench.action.tasks.reRunTask': 'Rerun Last Task', + 'workbench.action.tasks.restartTask': 'Restart Running Task', + 'workbench.action.tasks.terminate': 'Terminate Task', + 'workbench.action.tasks.showTasks': 'Show Running Tasks', + 'workbench.action.tasks.showLog': 'Show Task Log', + 'task.contribute': 'Contribute', + 'task.cannotFindTask': 'Cannot find task for {0}. Press Enter key to return.', + // extension contribute ...browserViews, }, diff --git a/packages/i18n/src/common/zh-CN.lang.ts b/packages/i18n/src/common/zh-CN.lang.ts index 0a0dff4aa2..4726c47758 100644 --- a/packages/i18n/src/common/zh-CN.lang.ts +++ b/packages/i18n/src/common/zh-CN.lang.ts @@ -848,6 +848,7 @@ export const localizationBundle = { 'TaskService.pickRunTask': '选择要运行的任务', 'TerminalTaskSystem.terminalName': '任务 - {0}', 'terminal.integrated.exitedWithCode': '终端进程已终止,退出代码: {0}', + // reuseTerminal: '终端将被任务重用,按 r 键重新执行该任务,按其他任意键关闭。', reuseTerminal: '终端将被任务重用,按任意键关闭。', 'toolbar-customize.buttonDisplay.description': '按钮展示形式', @@ -1019,8 +1020,15 @@ export const localizationBundle = { 'connection.stop.rtt': '关闭通信延迟检查', 'debug.terminal.label': '创建 Javascript Debug Terminal', - 'command.runTask': '运行任务', - + 'workbench.action.tasks.runTask': '运行任务', + 'workbench.action.tasks.reRunTask': '执行上次运行的任务', + 'workbench.action.tasks.restartTask': '重新开始运行中的任务', + 'workbench.action.tasks.terminate': '终止任务', + 'workbench.action.tasks.showTasks': '展示任务', + 'workbench.action.tasks.showLog': '展示任务日志', + + 'task.contribute': '贡献', + 'task.cannotFindTask': '未找到 {0} 的任务,按回车键返回', // extension contribute ...browserViews, }, diff --git a/packages/task/src/browser/task-executor.ts b/packages/task/src/browser/task-executor.ts new file mode 100644 index 0000000000..a810a5b202 --- /dev/null +++ b/packages/task/src/browser/task-executor.ts @@ -0,0 +1,305 @@ +import { Injectable, Autowired } from '@opensumi/di'; +import { + Event, + formatLocalize, + Disposable, + Deferred, + strings, + Emitter, + DisposableCollection, + ProblemMatch, + ProblemMatchData, +} from '@opensumi/ide-core-common'; +import { + ITerminalController, + ITerminalGroupViewService, + ITerminalClient, + ITerminalService, + IShellLaunchConfig, +} from '@opensumi/ide-terminal-next/lib/common'; + +import { ITaskExecutor } from '../common'; +import { Task } from '../common/task'; + +import { ProblemCollector } from './problem-collector'; + +const { removeAnsiEscapeCodes } = strings; + +export enum TaskStatus { + PROCESS_INIT, + PROCESS_READY, + PROCESS_RUNNING, + PROCESS_EXITED, +} +function rangeAreEqual(a, b) { + return ( + a.start.line === b.start.line && + a.start.character === b.start.character && + a.end.line === b.end.line && + a.end.character === b.end.character + ); +} + +function problemAreEquals(a: ProblemMatchData | ProblemMatch, b: ProblemMatchData | ProblemMatch) { + return ( + a.resource?.toString() === b.resource?.toString() && + a.description.owner === b.description.owner && + a.description.severity === b.description.severity && + a.description.source === b.description.source && + (a as ProblemMatchData)?.marker.code === (b as ProblemMatchData)?.marker.code && + (a as ProblemMatchData)?.marker.message === (b as ProblemMatchData)?.marker.message && + (a as ProblemMatchData)?.marker.source === (b as ProblemMatchData)?.marker.source && + rangeAreEqual((a as ProblemMatchData).marker.range, (b as ProblemMatchData).marker.range) + ); +} + +@Injectable({ multiple: true }) +export class TerminalTaskExecutor extends Disposable implements ITaskExecutor { + @Autowired(ITerminalGroupViewService) + protected readonly terminalView: ITerminalGroupViewService; + + @Autowired(ITerminalController) + protected readonly terminalController: ITerminalController; + + @Autowired(ITerminalService) + protected readonly terminalService: ITerminalService; + + private _terminalClient: ITerminalClient | undefined; + + get terminalClient() { + return this._terminalClient; + } + + set terminalClient(v) { + // 重新赋值之前,先清除之前的事件绑定,否则给 this.terminalClient 赋值之后就无法清除以前的事件监听了。 + this.resetEventDispose(); + this._terminalClient = v; + } + + private pid: number | undefined; + + private exitDefer: Deferred<{ exitCode?: number }> = new Deferred(); + + private _onDidTerminalCreated: Emitter = new Emitter(); + public onDidTerminalCreated: Event = this._onDidTerminalCreated.event; + + private _onDidTaskProcessExit: Emitter = new Emitter(); + public onDidTaskProcessExit: Event = this._onDidTaskProcessExit.event; + + private _onDidBackgroundTaskBegin: Emitter = new Emitter(); + public onDidBackgroundTaskBegin: Event = this._onDidBackgroundTaskBegin.event; + + private _onDidBackgroundTaskEnd: Emitter = new Emitter(); + public onDidBackgroundTaskEnd: Event = this._onDidBackgroundTaskEnd.event; + + private _onDidProblemMatched: Emitter = new Emitter(); + public onDidProblemMatched: Event = this._onDidProblemMatched.event; + + private _onDidTerminalWidgetRemove: Emitter = new Emitter(); + public onDidTerminalWidgetRemove: Event = this._onDidTerminalWidgetRemove.event; + + public processReady: Deferred = new Deferred(); + + private processExited = false; + + private eventToDispose: DisposableCollection = new DisposableCollection(); + resetEventDispose() { + this.eventToDispose.dispose(); + this.eventToDispose = new DisposableCollection(); + } + + public taskStatus: TaskStatus = TaskStatus.PROCESS_INIT; + + constructor( + private task: Task, + private shellLaunchConfig: IShellLaunchConfig, + private collector: ProblemCollector, + public executorId: number, + ) { + super(); + + this.addDispose( + this.terminalView.onWidgetDisposed((e) => { + if (this.terminalClient && e.id === this.terminalClient.id) { + this._onDidTerminalWidgetRemove.fire(); + } + }), + ); + } + + terminate(): Promise<{ success: boolean }> { + return new Promise((resolve) => { + if (this.terminalClient) { + this.terminalClient.dispose(); + if (this.processExited) { + // 如果在调 terminate 之前进程已经退出,直接删掉 terminalWidget 即可 + this.terminalView.removeWidget(this.terminalClient.id); + resolve({ success: true }); + } else { + this.terminalService.onExit((e) => { + if (e.sessionId === this.terminalClient?.id) { + this.terminalView.removeWidget(this.terminalClient.id); + resolve({ success: true }); + } + }); + } + } else { + resolve({ success: true }); + } + }); + } + + private handleTaskExit(code?: number) { + if (!this.terminalClient) { + return; + } + const { term } = this.terminalClient; + term.options.disableStdin = true; + + term.writeln(`\r\n${formatLocalize('terminal.integrated.exitedWithCode', code)}`); + term.writeln(`\r\n\x1b[1m${formatLocalize('reuseTerminal')}\x1b[0m\r\n`); + this._onDidTaskProcessExit.fire(code); + + // 按任意键退出 + this.eventToDispose.push( + this.terminalClient?.term.onKey(() => { + this.terminalClient?.id && this.terminalView.removeWidget(this.terminalClient.id); + }), + ); + } + + /** + * 监听 Terminal 的相关事件,一个 Executor 仅需监听一次即可,否则多次监听会导致输出重复内容。 + * 注意里面的 event 用完要及时 dispose + */ + private bindTerminalClientEvent() { + if (!this.terminalClient) { + return; + } + this.resetEventDispose(); + this.eventToDispose.push( + this.terminalClient.onOutput((e) => { + const output = removeAnsiEscapeCodes(e.data.toString()); + const isBegin = this.collector.matchBeginMatcher(output); + if (isBegin) { + this._onDidBackgroundTaskBegin.fire(); + } + + // process multi-line output + const lines = output.split(/\r?\n/g).filter((e) => e); + const markerResults: ProblemMatch[] = []; + for (const l of lines) { + const markers = this.collector.processLine(l); + if (markers && markers.length > 0) { + for (const marker of markers) { + const existing = markerResults.findIndex((e) => problemAreEquals(e, marker)); + if (existing === -1) { + markerResults.push(marker); + } + } + } + } + + if (markerResults.length > 0) { + this._onDidProblemMatched.fire(markerResults); + } + + const isEnd = this.collector.matchEndMatcher(output); + if (isEnd) { + this._onDidBackgroundTaskEnd.fire(); + } + }), + ); + + this.eventToDispose.push( + this.terminalClient.onExit(async (e) => { + if (e.id === this.terminalClient?.id && this.taskStatus !== TaskStatus.PROCESS_EXITED) { + this.taskStatus = TaskStatus.PROCESS_EXITED; + this.handleTaskExit(e.code); + this.processExited = true; + this.exitDefer.resolve({ exitCode: e.code }); + } + }), + ); + } + + private async createTerminal(reuse?: boolean) { + if (reuse && this.terminalClient) { + this.terminalClient.updateLaunchConfig(this.shellLaunchConfig); + this.terminalClient.reset(); + } else { + this.terminalClient = await this.terminalController.createTerminalWithWidget({ + options: this.shellLaunchConfig, + closeWhenExited: false, + isTaskExecutor: true, + taskId: this.task._id, + beforeCreate: (terminalId) => { + this._onDidTerminalCreated.fire(terminalId); + }, + }); + } + this.bindTerminalClientEvent(); + this.terminalController.showTerminalPanel(); + } + + async attach(terminalClient: ITerminalClient): Promise<{ exitCode?: number }> { + this.taskStatus = TaskStatus.PROCESS_READY; + this.terminalClient = terminalClient; + this.shellLaunchConfig = terminalClient.launchConfig; + this.bindTerminalClientEvent(); + this.taskStatus = TaskStatus.PROCESS_RUNNING; + this.pid = await this.terminalClient?.pid; + this.processReady.resolve(); + + this._onDidTerminalCreated.fire(terminalClient.id); + + return this.exitDefer.promise; + } + + async execute(task: Task, reuse?: boolean): Promise<{ exitCode?: number }> { + this.taskStatus = TaskStatus.PROCESS_READY; + + await this.createTerminal(reuse); + + this.terminalClient?.term.writeln(`\x1b[3m> Executing task: ${task._label} <\x1b[0m\n`); + const { args } = this.shellLaunchConfig; + + // extensionTerminal 由插件自身接管,不需要执行和输出 Command + if (!this.shellLaunchConfig.isExtensionOwnedTerminal && args) { + this.terminalClient?.term.writeln(`\x1b[3m> Command: ${typeof args === 'string' ? args : args[1]} <\x1b[0m\n`); + } + + await this.terminalClient?.attached.promise; + this.taskStatus = TaskStatus.PROCESS_RUNNING; + this.pid = await this.terminalClient?.pid; + this.processReady.resolve(); + this.terminalClient?.term.write('\x1b[G'); + return this.exitDefer.promise; + } + + get processId(): number | undefined { + return this.pid; + } + + get terminalId(): string | undefined { + return this.terminalClient && this.terminalClient.id; + } + + get widgetId(): string | undefined { + return this.terminalClient && this.terminalClient.widget.id; + } + + public updateLaunchConfig(launchConfig: IShellLaunchConfig) { + this.shellLaunchConfig = launchConfig; + } + + public updateProblemCollector(collector: ProblemCollector) { + this.collector = collector; + } + + public reset() { + this.resetEventDispose(); + this.taskStatus = TaskStatus.PROCESS_INIT; + this.exitDefer = new Deferred(); + } +} diff --git a/packages/task/src/browser/task.contribution.ts b/packages/task/src/browser/task.contribution.ts index 99d5e6d81b..8c989623bb 100644 --- a/packages/task/src/browser/task.contribution.ts +++ b/packages/task/src/browser/task.contribution.ts @@ -16,12 +16,21 @@ import { ITaskService } from '../common'; import { schema, taskSchemaUri } from './task.schema'; +const Category = 'Tasks'; + @Domain(CommandContribution, JsonSchemaContribution) export class TaskContribution extends WithEventBus implements CommandContribution, JsonSchemaContribution { + // 因为部分插件会执行 Task 相关的命令,所以如果你要添加 Task 相关的命令: + // 请注意要和 VSCode 的同功能命名对齐,可以通过插件进程的 delegate 逻辑做命令调用的转发 static readonly RUN_TASK_COMMAND: Command = { id: 'workbench.action.tasks.runTask', - label: '%command.runTask%', - category: 'Task', + label: '%workbench.action.tasks.runTask%', + category: Category, + }; + static readonly RERUN_TASK: Command = { + id: 'workbench.action.tasks.reRunTask', + label: '%workbench.action.tasks.reRunTask%', + category: Category, }; @Autowired(ITerminalController) @@ -52,5 +61,10 @@ export class TaskContribution extends WithEventBus implements CommandContributio this.taskService.runTaskCommand(); }, }); + commandRegister.registerCommand(TaskContribution.RERUN_TASK, { + execute: () => { + this.taskService.rerunLastTask(); + }, + }); } } diff --git a/packages/task/src/browser/task.service.ts b/packages/task/src/browser/task.service.ts index e63cf950c5..5b923c2afb 100644 --- a/packages/task/src/browser/task.service.ts +++ b/packages/task/src/browser/task.service.ts @@ -19,7 +19,6 @@ import { Event, IProblemPatternRegistry, Emitter, - WithEventBus, platform, } from '@opensumi/ide-core-common'; import { OutputChannel } from '@opensumi/ide-output/lib/browser/output.channel'; @@ -119,7 +118,7 @@ export class TaskService extends Disposable implements ITaskService { constructor() { super(); - this.outputChannel = this.outputService.getChannel(localize('task.outputchannel.name')); + this.outputChannel = this.outputService.getChannel(localize('task.output.channel')); this.providers = new Map(); this.providerTypes = new Map(); this.addDispose([ @@ -144,7 +143,7 @@ export class TaskService extends Disposable implements ITaskService { public async runTaskCommand() { const groupedTaskSet: TaskSet[] = await this.getGroupedTasks(); const workspaceTasks = await this.getWorkspaceTasks(groupedTaskSet); - const [workspaces, grouped] = this.combineQuickItems(groupedTaskSet, workspaceTasks!); + const [workspaces, grouped] = this.combineQuickItems(groupedTaskSet, workspaceTasks); this.quickOpenService.open( { onType: (lookFor: string, acceptor) => acceptor([...workspaces, ...grouped]), @@ -239,7 +238,7 @@ export class TaskService extends Disposable implements ITaskService { if (this.runningTasks.has(task._id)) { this.runningTasks.delete(task._id); } - this.outputChannel.appendLine(`task ${task._label} done, exit code ${res.exitCode}`); + this.outputChannel.appendLine(`Task ${task._label} done, exit code ${res.exitCode}`); }); this.runningTasks.set(task._id, task); @@ -313,7 +312,7 @@ export class TaskService extends Disposable implements ITaskService { private toQuickOpenGroupItem(showBorder: boolean, run, type?: string): QuickOpenItem { return new QuickOpenItem({ - groupLabel: showBorder ? '贡献' : undefined, + groupLabel: showBorder ? formatLocalize('task.contribute') : undefined, run, showBorder, label: type, @@ -322,7 +321,10 @@ export class TaskService extends Disposable implements ITaskService { }); } - private combineQuickItems(contributedTaskSet: TaskSet[], workspaceTasks: Map) { + private combineQuickItems( + contributedTaskSet: TaskSet[], + workspaceTasks: Map | undefined, + ) { const groups: QuickOpenItem[] = []; const workspace: QuickOpenItem[] = []; let showBorder = true; @@ -335,7 +337,7 @@ export class TaskService extends Disposable implements ITaskService { return acceptor([ new QuickOpenItem({ value: 'none', - label: `未找到 ${taskSet.type} 的任务,按回车键返回`, + label: formatLocalize('task.cannotFindTask', taskSet.type), run: (mode: Mode) => { if (mode === Mode.OPEN) { return true; @@ -421,7 +423,7 @@ export class TaskService extends Disposable implements ITaskService { const parseResult = parse( { uri: folderUri, name: folderUri.path, index: 0 }, platform, - tasksConfig!, + tasksConfig, problemReporter, this.taskDefinitionRegistry, this.problemMatcher, @@ -436,11 +438,10 @@ export class TaskService extends Disposable implements ITaskService { byIdentifier: Object.create(null), }; for (const task of parseResult.configured) { - // @ts-ignore customizedTasks.byIdentifier[task.configures._key] = task; } } - taskSet.push(...parseResult.custom!); + taskSet.push(...parseResult.custom); /** * Converter configuringTask to customTask */ @@ -477,6 +478,10 @@ export class TaskService extends Disposable implements ITaskService { this.outputChannel.appendLine('There are task errors. See the output for details.'); } + public rerunLastTask() { + return this.taskSystem.rerun(); + } + public registerTaskProvider(provider: ITaskProvider, type: string): IDisposable { const handler = (this.providerHandler += 1); this.providers.set(handler, provider); diff --git a/packages/task/src/browser/terminal-task-system.ts b/packages/task/src/browser/terminal-task-system.ts index 5476840c9e..5c52a53a0b 100644 --- a/packages/task/src/browser/terminal-task-system.ts +++ b/packages/task/src/browser/terminal-task-system.ts @@ -4,30 +4,18 @@ import { formatLocalize, IProblemMatcherRegistry, Disposable, - Deferred, ProblemMatcher, isString, - strings, Emitter, - DisposableCollection, - ProblemMatch, - ProblemMatchData, objects, path, } from '@opensumi/ide-core-common'; -import { - TerminalOptions, - ITerminalController, - ITerminalGroupViewService, - ITerminalClient, - ITerminalService, -} from '@opensumi/ide-terminal-next/lib/common'; +import { ITerminalClient, IShellLaunchConfig } from '@opensumi/ide-terminal-next/lib/common'; import { IVariableResolverService } from '@opensumi/ide-variable'; import { ITaskSystem, ITaskExecuteResult, - ITaskExecutor, TaskExecuteKind, IActivateTaskExecutorData, TaskTerminateResponse, @@ -44,281 +32,11 @@ import { import { CustomTask } from '../common/task'; import { ProblemCollector } from './problem-collector'; +import { TaskStatus, TerminalTaskExecutor } from './task-executor'; const { deepClone } = objects; -const { removeAnsiEscapeCodes } = strings; const { Path } = path; -enum TaskStatus { - PROCESS_INIT, - PROCESS_READY, - PROCESS_RUNNING, - PROCESS_EXITED, -} - -function rangeAreEqual(a, b) { - return ( - a.start.line === b.start.line && - a.start.character === b.start.character && - a.end.line === b.end.line && - a.end.character === b.end.character - ); -} - -function problemAreEquals(a: ProblemMatchData | ProblemMatch, b: ProblemMatchData | ProblemMatch) { - return ( - a.resource?.toString() === b.resource?.toString() && - a.description.owner === b.description.owner && - a.description.severity === b.description.severity && - a.description.source === b.description.source && - (a as ProblemMatchData)?.marker.code === (b as ProblemMatchData)?.marker.code && - (a as ProblemMatchData)?.marker.message === (b as ProblemMatchData)?.marker.message && - (a as ProblemMatchData)?.marker.source === (b as ProblemMatchData)?.marker.source && - rangeAreEqual((a as ProblemMatchData).marker.range, (b as ProblemMatchData).marker.range) - ); -} - -@Injectable({ multiple: true }) -export class TerminalTaskExecutor extends Disposable implements ITaskExecutor { - @Autowired(ITerminalGroupViewService) - protected readonly terminalView: ITerminalGroupViewService; - - @Autowired(ITerminalController) - protected readonly terminalController: ITerminalController; - - @Autowired(ITerminalService) - protected readonly terminalService: ITerminalService; - - private terminalClient: ITerminalClient | undefined; - - private pid: number | undefined; - - private exitDefer: Deferred<{ exitCode?: number }> = new Deferred(); - - private _onDidTerminalCreated: Emitter = new Emitter(); - - private _onDidTaskProcessExit: Emitter = new Emitter(); - - private _onDidBackgroundTaskBegin: Emitter = new Emitter(); - public onDidBackgroundTaskBegin: Event = this._onDidBackgroundTaskBegin.event; - - private _onDidBackgroundTaskEnd: Emitter = new Emitter(); - public onDidBackgroundTaskEnd: Event = this._onDidBackgroundTaskEnd.event; - - private _onDidProblemMatched: Emitter = new Emitter(); - public onDidProblemMatched: Event = this._onDidProblemMatched.event; - - private _onDidTerminalWidgetRemove: Emitter = new Emitter(); - - public onDidTerminalCreated: Event = this._onDidTerminalCreated.event; - - public onDidTerminalWidgetRemove: Event = this._onDidTerminalWidgetRemove.event; - - public onDidTaskProcessExit: Event = this._onDidTaskProcessExit.event; - - public processReady: Deferred = new Deferred(); - - private processExited = false; - - private disposableCollection: DisposableCollection = new DisposableCollection(); - - public taskStatus: TaskStatus = TaskStatus.PROCESS_INIT; - - constructor( - private task: Task, - private terminalOptions: TerminalOptions, - private collector: ProblemCollector, - public executorId: number, - ) { - super(); - - this.addDispose( - this.terminalView.onWidgetDisposed((e) => { - if (this.terminalClient && e.id === this.terminalClient.id) { - this._onDidTerminalWidgetRemove.fire(); - } - }), - ); - } - - terminate(): Promise<{ success: boolean }> { - return new Promise((resolve) => { - if (this.terminalClient) { - this.terminalClient.dispose(); - if (this.processExited) { - // 如果在调 terminate 之前进程已经退出,直接删掉 terminalWidget 即可 - this.terminalView.removeWidget(this.terminalClient.id); - resolve({ success: true }); - } else { - this.terminalService.onExit((e) => { - if (e.sessionId === this.terminalClient?.id) { - this.terminalView.removeWidget(this.terminalClient.id); - resolve({ success: true }); - } - }); - } - } else { - resolve({ success: true }); - } - }); - } - - private onTaskExit(code?: number) { - const { term, id } = this.terminalClient!; - term.setOption('disableStdin', true); - term.writeln(formatLocalize('terminal.integrated.exitedWithCode', code)); - term.writeln(`\r\n\x1b[1m${formatLocalize('reuseTerminal')}\x1b[0m`); - this._onDidTaskProcessExit.fire(code); - this.disposableCollection.push( - term.onKey(() => { - this.terminalView.removeWidget(id); - }), - ); - } - - private bindTerminalClientEvent() { - this.addDispose( - this.terminalClient?.onOutput((e) => { - const output = removeAnsiEscapeCodes(e.data.toString()); - const isBegin = this.collector.matchBeginMatcher(output); - if (isBegin) { - this._onDidBackgroundTaskBegin.fire(); - } - - // process multi-line output - const lines = output.split(/\r?\n/g).filter((e) => e); - const markerResults: ProblemMatch[] = []; - for (const l of lines) { - const markers = this.collector.processLine(l); - if (markers && markers.length > 0) { - for (const marker of markers) { - const existing = markerResults.findIndex((e) => problemAreEquals(e, marker)); - if (existing === -1) { - markerResults.push(marker); - } - } - } - } - - if (markerResults.length > 0) { - this._onDidProblemMatched.fire(markerResults); - } - - const isEnd = this.collector.matchEndMatcher(output); - if (isEnd) { - this._onDidBackgroundTaskEnd.fire(); - } - }) || Disposable.NULL, - ); - - this.disposableCollection.push( - this.terminalClient?.onExit(async (e) => { - if (e.id === this.terminalClient?.id && this.taskStatus !== TaskStatus.PROCESS_EXITED) { - this.onTaskExit(e.code); - this.processExited = true; - this.taskStatus = TaskStatus.PROCESS_EXITED; - this.exitDefer.resolve({ exitCode: e.code }); - } - }) || Disposable.NULL, - ); - - this.disposableCollection.push( - this.terminalService.onExit(async (e) => { - if (e.sessionId === this.terminalClient?.id && this.taskStatus !== TaskStatus.PROCESS_EXITED) { - await this.processReady.promise; - this.onTaskExit(e.code); - this.processExited = true; - this.taskStatus = TaskStatus.PROCESS_EXITED; - this.exitDefer.resolve({ exitCode: e.code }); - } - }), - ); - } - - private async createTerminal(reuse?: boolean) { - if (reuse && this.terminalClient) { - this.terminalClient.updateOptions(this.terminalOptions); - this.terminalClient.reset(); - } else { - this.terminalClient = await this.terminalController.createClientWithWidget2({ - terminalOptions: this.terminalOptions, - closeWhenExited: false, - isTaskExecutor: true, - taskId: this.task._id, - beforeCreate: (terminalId) => { - this._onDidTerminalCreated.fire(terminalId); - }, - }); - } - - this.terminalController.showTerminalPanel(); - this.bindTerminalClientEvent(); - } - - async attach(terminalClient: ITerminalClient): Promise<{ exitCode?: number }> { - this.taskStatus = TaskStatus.PROCESS_READY; - this.terminalClient = terminalClient; - this.terminalOptions = terminalClient.options; - this.bindTerminalClientEvent(); - this.taskStatus = TaskStatus.PROCESS_RUNNING; - this.pid = await this.terminalClient?.pid; - this.processReady.resolve(); - - this._onDidTerminalCreated.fire(terminalClient.id); - - return this.exitDefer.promise; - } - - async execute(task: Task, reuse?: boolean): Promise<{ exitCode?: number }> { - this.taskStatus = TaskStatus.PROCESS_READY; - - await this.createTerminal(reuse); - - this.terminalClient?.term.writeln(`\x1b[3m> Executing task: ${task._label} <\x1b[0m\n`); - const { shellArgs } = this.terminalOptions; - - // extensionTerminal 由插件自身接管,不需要执行和输出 Command - if (!this.terminalOptions.isExtensionTerminal && shellArgs) { - this.terminalClient?.term.writeln( - `\x1b[3m> Command: ${typeof shellArgs === 'string' ? shellArgs : shellArgs[1]} <\x1b[0m\n`, - ); - } - - await this.terminalClient?.attached.promise; - this.taskStatus = TaskStatus.PROCESS_RUNNING; - this.pid = await this.terminalClient?.pid; - this.processReady.resolve(); - this.terminalClient?.term.write('\n\x1b[G'); - return this.exitDefer.promise; - } - - get processId(): number | undefined { - return this.pid; - } - - get terminalId(): string | undefined { - return this.terminalClient && this.terminalClient.id; - } - - get widgetId(): string | undefined { - return this.terminalClient && this.terminalClient.widget.id; - } - - public updateTerminalOptions(terminalOptions: TerminalOptions) { - this.terminalOptions = terminalOptions; - } - - public updateProblemCollector(collector: ProblemCollector) { - this.collector = collector; - } - - public reset() { - this.disposableCollection.dispose(); - this.taskStatus = TaskStatus.PROCESS_INIT; - this.exitDefer = new Deferred(); - } -} - @Injectable() export class TerminalTaskSystem extends Disposable implements ITaskSystem { @Autowired(INJECTOR_TOKEN) @@ -332,6 +50,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { private executorId = 0; + private lastTask: CustomTask | ContributedTask | undefined; protected currentTask: Task; private activeTaskExecutors: Map = new Map(); @@ -395,17 +114,17 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { result.push(await this.resolveVariable(arg)); } } - return { shellArgs: ['-c', `${result.join(' ')}`] }; + return { args: ['-c', `${result.join(' ')}`] }; } private findAvailableExecutor(): TerminalTaskExecutor | undefined { return this.taskExecutors.find((e) => e.taskStatus === TaskStatus.PROCESS_EXITED); } - private async createTaskExecutor(task: CustomTask | ContributedTask, options: TerminalOptions) { + private async createTaskExecutor(task: CustomTask | ContributedTask, launchConfig: IShellLaunchConfig) { const matchers = await this.resolveMatchers(task.configurationProperties.problemMatchers); const collector = new ProblemCollector(matchers); - const executor = this.injector.get(TerminalTaskExecutor, [task, options, collector, this.executorId]); + const executor = this.injector.get(TerminalTaskExecutor, [task, launchConfig, collector, this.executorId]); this.executorId += 1; this.taskExecutors.push(executor); this.addDispose( @@ -421,8 +140,9 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { task: CustomTask | ContributedTask, terminalClient: ITerminalClient, ): Promise { - const taskExecutor = await this.createTaskExecutor(task, terminalClient.options); + const taskExecutor = await this.createTaskExecutor(task, terminalClient.launchConfig); const p = taskExecutor.attach(terminalClient); + this.lastTask = task; return { task, kind: TaskExecuteKind.Started, @@ -436,26 +156,28 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { const matchers = await this.resolveMatchers(task.configurationProperties.problemMatchers); const collector = new ProblemCollector(matchers); - const { shellArgs } = await this.buildShellConfig(task.command); + const { args } = await this.buildShellConfig(task.command); - const terminalOptions: TerminalOptions = { + const launchConfig: IShellLaunchConfig = { name: this.createTerminalName(task), - shellArgs, - isExtensionTerminal: isCustomExecution, + args, + isExtensionOwnedTerminal: isCustomExecution, env: task.command.options?.env || {}, cwd: task.command.options?.cwd ? await this.resolveVariable(task.command.options?.cwd) : await this.resolveVariable('${workspaceFolder}'), + // 不需要历史记录 + disablePreserveHistory: true, }; let executor: TerminalTaskExecutor | undefined = this.findAvailableExecutor(); let reuse = false; if (!executor) { - executor = await this.createTaskExecutor(task, terminalOptions); + executor = await this.createTaskExecutor(task, launchConfig); } else { reuse = true; executor.updateProblemCollector(collector); - executor.updateTerminalOptions(terminalOptions); + executor.updateLaunchConfig(launchConfig); executor.reset(); } @@ -507,6 +229,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { await executor.processReady.promise; this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.ProcessStarted, task, executor.processId)); + this.lastTask = task; return { task, kind: TaskExecuteKind.Started, @@ -577,11 +300,9 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { } return result; } - getActiveTasks(): Task[] { return Array.from(this.activeTaskExecutors.values()).map((e) => e.task); } - async terminate(task: Task): Promise { const key = task.getMapKey(); const activeExecutor = this.activeTaskExecutors.get(key); @@ -592,9 +313,8 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { this.activeTaskExecutors.delete(key); return { task, success }; } - - rerun(): import('../common').ITaskExecuteResult | undefined { - throw new Error('Method not implemented.'); + async rerun(): Promise { + return this.lastTask && (await this.executeTask(this.lastTask)); } isActive(): Promise { throw new Error('Method not implemented.'); @@ -602,21 +322,19 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { isActiveSync(): boolean { throw new Error('Method not implemented.'); } - - getBusyTasks(): import('../common/task').Task[] { + getBusyTasks(): Task[] { throw new Error('Method not implemented.'); } canAutoTerminate(): boolean { throw new Error('Method not implemented.'); } - - terminateAll(): Promise { + terminateAll(): Promise { throw new Error('Method not implemented.'); } - revealTask(task: import('../common/task').Task): boolean { + revealTask(task: Task): boolean { throw new Error('Method not implemented.'); } - customExecutionComplete(task: import('../common/task').Task, result: number): Promise { + customExecutionComplete(task: Task, result: number): Promise { throw new Error('Method not implemented.'); } } diff --git a/packages/task/src/common/index.ts b/packages/task/src/common/index.ts index 0d66b2c525..77d19638dc 100644 --- a/packages/task/src/common/index.ts +++ b/packages/task/src/common/index.ts @@ -1,7 +1,7 @@ import { IJSONSchemaMap } from '@opensumi/ide-core-browser'; import { IDisposable, Event, URI, TaskIdentifier, Uri, Deferred } from '@opensumi/ide-core-common'; import { UriComponents } from '@opensumi/ide-editor'; -import { ITerminalClient, TerminalOptions } from '@opensumi/ide-terminal-next/lib/common'; +import { IShellLaunchConfig, ITerminalClient } from '@opensumi/ide-terminal-next/lib/common'; // eslint-disable-next-line import/no-restricted-paths import type { ProblemCollector } from '../browser/problem-collector'; @@ -11,7 +11,7 @@ import { Task, ConfiguringTask, ContributedTask, TaskSet, KeyedTaskIdentifier, T // eslint-disable-next-line @typescript-eslint/no-empty-interface interface TaskMap {} -interface TaskFileter { +interface TaskFilter { version?: string; type?: string; } @@ -113,7 +113,7 @@ export interface ITaskExecutor { execute(task: Task, reuse?: boolean): Promise<{ exitCode?: number }>; reset(): void; terminate(): Promise<{ success: boolean }>; - updateTerminalOptions(options: TerminalOptions): void; + updateLaunchConfig(launchConfig: IShellLaunchConfig): void; updateProblemCollector(collector: ProblemCollector): void; onDidTerminalWidgetRemove: Event; onDidTaskProcessExit: Event; @@ -131,7 +131,7 @@ export interface ITaskSystem { onDidProblemMatched: Event; attach(task: Task | ConfiguringTask, terminalClient: ITerminalClient): Promise; run(task: Task | ConfiguringTask): Promise; - rerun(): ITaskExecuteResult | undefined; + rerun(): Promise; isActive(): Promise; isActiveSync(): boolean; getActiveTasks(): Task[]; @@ -149,12 +149,13 @@ export interface ITaskService { run(task: Task | ConfiguringTask): Promise; runTaskCommand(): void; + rerunLastTask(): void; updateWorkspaceTasks(tasks: TaskMap): void; registerTaskProvider(provider: ITaskProvider, type: string): IDisposable; - tasks(filter?: TaskFileter): Promise; + tasks(filter?: TaskFilter): Promise; getTask(workspaceFolder: Uri, identifier: string | TaskIdentifier, compareId?: boolean): Promise; diff --git a/packages/terminal-next/__tests__/browser/client.test.ts b/packages/terminal-next/__tests__/browser/client.test.ts index f004ad85d0..60428578cb 100644 --- a/packages/terminal-next/__tests__/browser/client.test.ts +++ b/packages/terminal-next/__tests__/browser/client.test.ts @@ -12,7 +12,6 @@ import { Disposable, FileUri, URI } from '@opensumi/ide-core-common'; import { IWorkspaceService } from '@opensumi/ide-workspace'; import { - ITerminalClientFactory, ITerminalGroupViewService, ITerminalClient, IWidget, @@ -39,7 +38,7 @@ describe('Terminal Client', () => { let proxy: httpProxy; let server: WebSocket.Server; let view: ITerminalGroupViewService; - let factory: ITerminalClientFactory; + let factory2: ITerminalClientFactory2; let workspaceService: IWorkspaceService; let root: URI | null; @@ -56,7 +55,7 @@ describe('Terminal Client', () => { isDirectory: true, }); resetPort(); - factory = injector.get(ITerminalClientFactory); + factory2 = injector.get(ITerminalClientFactory2); view = injector.get(ITerminalGroupViewService); server = createWsServer(); proxy = createProxyServer(); @@ -72,7 +71,7 @@ describe('Terminal Client', () => { return target[prop]; }, }); - client = await factory(widget, {}); + client = await factory2(widget, {}); client.addDispose( Disposable.create(async () => { if (root) { @@ -157,7 +156,7 @@ describe('Terminal Client', () => { client.clear(); }); - it('should use isExtentionOwnedTerminal to determine the terminal process', async () => { + it('should use isExtensionOwnedTerminal to determine the terminal process', async () => { let launchConfig1: IShellLaunchConfig | undefined; injector.mock( ITerminalInternalService, diff --git a/packages/terminal-next/__tests__/browser/inject.ts b/packages/terminal-next/__tests__/browser/inject.ts index 83b7f9358e..0c0b5e91fc 100644 --- a/packages/terminal-next/__tests__/browser/inject.ts +++ b/packages/terminal-next/__tests__/browser/inject.ts @@ -30,7 +30,7 @@ import { IThemeService } from '@opensumi/ide-theme'; import { IWorkspaceService } from '@opensumi/ide-workspace'; import { MockWorkspaceService } from '@opensumi/ide-workspace/lib/common/mocks'; -import { createTerminalClientFactory, createTerminalClientFactory2 } from '../../src/browser/terminal.client'; +import { createTerminalClientFactory2 } from '../../src/browser/terminal.client'; import { TerminalController } from '../../src/browser/terminal.controller'; import { TerminalInternalService } from '../../src/browser/terminal.internal.service'; import { TerminalNetworkService } from '../../src/browser/terminal.network'; @@ -39,7 +39,6 @@ import { TerminalGroupViewService } from '../../src/browser/terminal.view'; import { ITerminalService, ITerminalTheme, - ITerminalClientFactory, ITerminalClientFactory2, ITerminalController, ITerminalGroupViewService, @@ -129,10 +128,6 @@ export const injector = new MockInjector([ token: CorePreferences, useValue: mockPreferences, }, - { - token: ITerminalClientFactory, - useFactory: createTerminalClientFactory, - }, { token: ITerminalClientFactory2, useFactory: createTerminalClientFactory2, diff --git a/packages/terminal-next/src/browser/contribution/terminal.command.ts b/packages/terminal-next/src/browser/contribution/terminal.command.ts index e85ce052d7..cf5e76ecde 100644 --- a/packages/terminal-next/src/browser/contribution/terminal.command.ts +++ b/packages/terminal-next/src/browser/contribution/terminal.command.ts @@ -116,8 +116,8 @@ export class TerminalCommandContribution implements CommandContribution { registry.registerCommand(TERMINAL_COMMANDS.ADD, { execute: async () => { - await this.terminalController.createClientWithWidget2({ - terminalOptions: {}, + await this.terminalController.createTerminalWithWidget({ + options: {}, }); this.terminalController.showTerminalPanel(); }, diff --git a/packages/terminal-next/src/browser/contribution/terminal.keybinding.ts b/packages/terminal-next/src/browser/contribution/terminal.keybinding.ts index e6a45ab9d8..f420a20d76 100644 --- a/packages/terminal-next/src/browser/contribution/terminal.keybinding.ts +++ b/packages/terminal-next/src/browser/contribution/terminal.keybinding.ts @@ -4,7 +4,7 @@ import { RawContextKey } from '@opensumi/ide-core-browser/lib/raw-context-key'; import { Domain, isWindows } from '@opensumi/ide-core-common'; @Domain(KeybindingContribution) -export class TerminalKeybindinngContribution implements KeybindingContribution { +export class TerminalKeybindingContribution implements KeybindingContribution { registerKeybindings(registry: KeybindingRegistry) { registry.registerKeybinding({ command: TERMINAL_COMMANDS.OPEN_SEARCH.id, diff --git a/packages/terminal-next/src/browser/contribution/terminal.view.ts b/packages/terminal-next/src/browser/contribution/terminal.view.ts index 06f11232c8..17ea108a17 100644 --- a/packages/terminal-next/src/browser/contribution/terminal.view.ts +++ b/packages/terminal-next/src/browser/contribution/terminal.view.ts @@ -27,7 +27,7 @@ export class TerminalRenderContribution implements ComponentContribution, TabBar id: TERMINAL_COMMANDS.CLEAR_CONTENT.id, command: TERMINAL_COMMANDS.CLEAR_CONTENT.id, viewId: TerminalRenderContribution.viewId, - tooltip: localize('terminal.menu.clearGroups'), + tooltip: localize('terminal.menu.clearCurrentContent'), }); registry.registerItem({ id: TERMINAL_COMMANDS.SPLIT.id, diff --git a/packages/terminal-next/src/browser/index.ts b/packages/terminal-next/src/browser/index.ts index f93e7f6b1b..df1063665e 100644 --- a/packages/terminal-next/src/browser/index.ts +++ b/packages/terminal-next/src/browser/index.ts @@ -8,7 +8,6 @@ import { ITerminalTheme, ITerminalServicePath, ITerminalProcessPath, - ITerminalClientFactory, ITerminalApiService, ITerminalSearchService, ITerminalGroupViewService, @@ -29,12 +28,12 @@ import { TerminalMenuContribution, TerminalLifeCycleContribution, TerminalRenderContribution, - TerminalKeybindinngContribution, + TerminalKeybindingContribution, TerminalNetworkContribution, TerminalPreferenceContribution, } from './contribution'; import { TerminalApiService } from './terminal.api'; -import { createTerminalClientFactory, createTerminalClientFactory2 } from './terminal.client'; +import { createTerminalClientFactory2 } from './terminal.client'; import { TerminalController } from './terminal.controller'; import { TerminalEnvironmentService } from './terminal.environment.service'; import { TerminalErrorService } from './terminal.error'; @@ -58,7 +57,7 @@ export class TerminalNextModule extends BrowserModule { TerminalRenderContribution, TerminalCommandContribution, TerminalMenuContribution, - TerminalKeybindinngContribution, + TerminalKeybindingContribution, TerminalNetworkContribution, TerminalPreferenceContribution, { @@ -109,10 +108,6 @@ export class TerminalNextModule extends BrowserModule { token: ITerminalRenderProvider, useClass: TerminalRenderProvider, }, - { - token: ITerminalClientFactory, - useFactory: createTerminalClientFactory, - }, { token: ITerminalClientFactory2, useFactory: createTerminalClientFactory2, diff --git a/packages/terminal-next/src/browser/terminal.addon.ts b/packages/terminal-next/src/browser/terminal.addon.ts index 50e2ddaa31..0b2e935917 100644 --- a/packages/terminal-next/src/browser/terminal.addon.ts +++ b/packages/terminal-next/src/browser/terminal.addon.ts @@ -151,7 +151,6 @@ export class AttachAddon extends Disposable implements ITerminalAddon { if (connection) { this._disposeConnection = new Disposable( connection.onData((data: string | ArrayBuffer) => { - // connection.onData 的时候对 lastInputTime 进行差值运算,统计最后一次输入到收到回复的时间间隔 let dataToWrite = data; if (typeof data === 'string') { const beforeProcessDataEvent = { data } as { data: string }; @@ -166,6 +165,8 @@ export class AttachAddon extends Disposable implements ITerminalAddon { this._onData.fire(dataToWrite); this._terminal.write(typeof dataToWrite === 'string' ? dataToWrite : new Uint8Array(dataToWrite)); + + // connection.onData 的时候对 lastInputTime 进行差值运算,统计最后一次输入到收到回复的时间间隔 if (this._lastInputTime) { const delta = Date.now() - this._lastInputTime; this._lastInputTime = 0; diff --git a/packages/terminal-next/src/browser/terminal.api.ts b/packages/terminal-next/src/browser/terminal.api.ts index 2dc726c213..83c6661f24 100644 --- a/packages/terminal-next/src/browser/terminal.api.ts +++ b/packages/terminal-next/src/browser/terminal.api.ts @@ -67,7 +67,7 @@ export class TerminalApiService implements ITerminalApiService { } async createTerminal(options: vscode.TerminalOptions, id?: string): Promise { - const client = await this.controller.createClientWithWidget2({ + const client = await this.controller.createTerminalWithWidgetByTerminalOptions({ terminalOptions: options, id, }); diff --git a/packages/terminal-next/src/browser/terminal.client.ts b/packages/terminal-next/src/browser/terminal.client.ts index 24970a732f..9be25eb562 100644 --- a/packages/terminal-next/src/browser/terminal.client.ts +++ b/packages/terminal-next/src/browser/terminal.client.ts @@ -38,17 +38,11 @@ import { ITerminalConnection, ITerminalExternalLinkProvider, ICreateTerminalOptions, - ITerminalProfile, IShellLaunchConfig, ITerminalProfileInternalService, } from '../common'; import { EnvironmentVariableServiceToken, IEnvironmentVariableService } from '../common/environmentVariable'; -import { - SupportedOptions, - ITerminalPreference, - CodeTerminalSettingId, - SupportedOptionsName, -} from '../common/preference'; +import { SupportedOptions, ITerminalPreference, CodeTerminalSettingId } from '../common/preference'; import { TerminalLinkManager } from './links/link-manager'; import { AttachAddon, DEFAULT_COL, DEFAULT_ROW } from './terminal.addon'; @@ -114,7 +108,7 @@ export class TerminalClient extends Disposable implements ITerminalClient { protected readonly view: ITerminalGroupViewService; @Autowired(ITerminalPreference) - protected readonly preference: ITerminalPreference; + protected readonly terminalPreference: ITerminalPreference; @Autowired(TerminalKeyBoardInputService) protected readonly keyboard: TerminalKeyBoardInputService; @@ -173,7 +167,7 @@ export class TerminalClient extends Disposable implements ITerminalClient { xtermOptions: { theme: this.theme.terminalTheme, ...this.internalService.getOptions(), - ...this.preference.toJSON(), + ...this.terminalPreference.toJSON(), }, }, ]); @@ -184,11 +178,11 @@ export class TerminalClient extends Disposable implements ITerminalClient { this.internalService.onError((error) => { this.messageService.error(error.message); if (error.launchConfig?.executable) { - this.updateOptions({ + this.updateTerminalName({ name: 'error: ' + error.launchConfig.executable, }); } else { - this.updateOptions({ + this.updateTerminalName({ name: 'error', }); } @@ -222,7 +216,7 @@ export class TerminalClient extends Disposable implements ITerminalClient { ); this.addDispose( - this.preference.onChange(async ({ name, value }) => { + this.terminalPreference.onChange(async ({ name, value }) => { if (!widget.show && !this._show) { this._show = new Deferred(); } @@ -272,53 +266,12 @@ export class TerminalClient extends Disposable implements ITerminalClient { ); } - /** - * 目前 core 内不再有代码调用这个函数,只有测试用例中会调用这里。 - * 后续移除掉该函数 - * @deprecated Please use `init2` instead. - */ - async init(widget: IWidget, options: TerminalOptions = {}) { - this._terminalOptions = options; - await this.init2(widget, { - config: this.controller.convertTerminalOptionsToLaunchConfig(options), - }); - } - convertProfileToLaunchConfig( - shellLaunchConfigOrProfile: IShellLaunchConfig | ITerminalProfile | undefined, - cwd?: Uri | string, - ): IShellLaunchConfig { - if (!shellLaunchConfigOrProfile) { - return {}; - } - // Profile was provided - if (shellLaunchConfigOrProfile && 'profileName' in shellLaunchConfigOrProfile) { - const profile = shellLaunchConfigOrProfile; - if (!profile.path) { - return shellLaunchConfigOrProfile; - } - return { - executable: profile.path, - args: profile.args, - env: profile.env, - // icon: profile.icon, - color: profile.color, - name: profile.overrideName ? profile.profileName : undefined, - cwd, - }; - } - - if (cwd) { - shellLaunchConfigOrProfile.cwd = cwd; - } - return shellLaunchConfigOrProfile; - } - async init2(widget: IWidget, options?: ICreateTerminalOptions) { this._uid = options?.id || widget.id; this.setupWidget(widget); if (!options || Object.keys(options).length === 0) { - // 应该是必定能 resolve 到 profile 的 + // Must be able to resolve a profile const defaultProfile = await this.terminalProfileInternalService.resolveDefaultProfile(); options = { id: this._uid, @@ -328,7 +281,7 @@ export class TerminalClient extends Disposable implements ITerminalClient { await this._checkWorkspace(); const cwd = options.cwd ?? (options?.config as IShellLaunchConfig)?.cwd ?? this._workspacePath; - const launchConfig = this.convertProfileToLaunchConfig(options.config, cwd); + const launchConfig = this.controller.convertProfileToLaunchConfig(options.config, cwd); this._launchConfig = launchConfig; if (this._launchConfig.__fromTerminalOptions) { this._terminalOptions = this._launchConfig.__fromTerminalOptions; @@ -373,8 +326,8 @@ export class TerminalClient extends Disposable implements ITerminalClient { return this.internalService.getProcessId(this.id); } - get options() { - return this._terminalOptions; + get launchConfig(): IShellLaunchConfig { + return this._launchConfig; } get createOptions() { @@ -490,10 +443,10 @@ export class TerminalClient extends Disposable implements ITerminalClient { } _prepare() { - this._attached?.reject(); - this._firstStdout?.reject(); - this._error?.reject(); - this._show?.reject(); + this._attached?.reject('TerminalClient is Re-initialization'); + this._firstStdout?.reject('TerminalClient is Re-initialization'); + this._error?.reject('TerminalClient is Re-initialization'); + this._show?.reject('TerminalClient is Re-initialization'); this._ready = false; this._hasOutput = false; this._attached = new Deferred(); @@ -566,11 +519,11 @@ export class TerminalClient extends Disposable implements ITerminalClient { private async attach() { if (!this._ready) { - return this._doAttach2(); + return this._doAttach(); } } - private async _doAttach2() { + private async _doAttach() { const sessionId = this.id; const { rows = DEFAULT_ROW, cols = DEFAULT_COL } = this.xterm.raw; @@ -633,7 +586,7 @@ export class TerminalClient extends Disposable implements ITerminalClient { * 这种情况可能会报错 */ try { - this.xterm.raw.setOption(name, value); + this.xterm.raw.options[name] = value; this._layout(); } catch (_e) { /** nothing */ @@ -725,11 +678,14 @@ export class TerminalClient extends Disposable implements ITerminalClient { updateTheme() { return this.xterm.updateTheme(this.theme.terminalTheme); } + updateLaunchConfig(launchConfig: IShellLaunchConfig) { + this._launchConfig = { + ...this._launchConfig, + ...launchConfig, + }; + } - updateOptions(options: TerminalOptions) { - this._terminalOptions = { ...this._terminalOptions, ...options }; - this._launchConfig = this.controller.convertTerminalOptionsToLaunchConfig(this._terminalOptions); - + updateTerminalName(options: { name: string }) { if (!this.name && !this._widget.name) { this._widget.name = options.name || this.name; } @@ -758,25 +714,6 @@ export class TerminalClient extends Disposable implements ITerminalClient { @Injectable() export class TerminalClientFactory { - /** - * 创建 terminal 实例最终都会调用该方法 - */ - static async createClient(injector: Injector, widget: IWidget, options?: TerminalOptions) { - // 每一个 widget.id 对应一个 TerminalClient - // 但是 TerminalClient 内部又依赖了一堆的其他要注入的,所以这里新创建一个 child injector - // 让 TerminalClient 依赖的所有类都重新初始化一遍 - - const child = injector.createChild([ - { - token: TerminalClient, - useClass: TerminalClient, - }, - ]); - - const client = child.get(TerminalClient); - await client.init(widget, options); - return client; - } /** * 创建 terminal 实例最终都会调用该方法 */ @@ -794,9 +731,6 @@ export class TerminalClientFactory { } } -export const createTerminalClientFactory = (injector: Injector) => (widget: IWidget, options?: TerminalOptions) => - TerminalClientFactory.createClient(injector, widget, options); - export const createTerminalClientFactory2 = (injector: Injector) => (widget: IWidget, options?: ICreateTerminalOptions) => TerminalClientFactory.createClient2(injector, widget, options); diff --git a/packages/terminal-next/src/browser/terminal.controller.ts b/packages/terminal-next/src/browser/terminal.controller.ts index 7d8df307f1..7654c28178 100644 --- a/packages/terminal-next/src/browser/terminal.controller.ts +++ b/packages/terminal-next/src/browser/terminal.controller.ts @@ -20,6 +20,7 @@ import { replaceLocalizePlaceholder, withNullAsUndefined, isThemeColor, + Uri, } from '@opensumi/ide-core-common'; import { WorkbenchEditorService } from '@opensumi/ide-editor'; import { IMainLayoutService } from '@opensumi/ide-main-layout'; @@ -29,7 +30,6 @@ import { IThemeService } from '@opensumi/ide-theme'; import { ITerminalController, ITerminalClient, - ITerminalClientFactory, IWidget, ITerminalInfo, ITerminalBrowserHistory, @@ -52,6 +52,8 @@ import { TerminalOptions, asTerminalIcon, TERMINAL_ID_SEPARATOR, + ICreateTerminalWithWidgetOptions, + ITerminalProfile, } from '../common'; import { TerminalContextKey } from './terminal.context-key'; @@ -98,9 +100,6 @@ export class TerminalController extends WithEventBus implements ITerminalControl @Autowired(ITerminalGroupViewService) protected readonly terminalView: TerminalGroupViewService; - @Autowired(ITerminalClientFactory) - protected readonly clientFactory: ITerminalClientFactory; - @Autowired(ITerminalClientFactory2) protected readonly clientFactory2: ITerminalClientFactory2; @@ -195,15 +194,8 @@ export class TerminalController extends WithEventBus implements ITerminalControl } private async _createClient(widget: IWidget, options?: ICreateTerminalOptions) { - let client: ITerminalClient; - - if (!options || (options as ICreateTerminalOptions).config || Object.keys(options).length === 0) { - client = await this.clientFactory2(widget, /** @type ICreateTerminalOptions */ options); - this.logger.log('create client with clientFactory2', client); - } else { - client = await this.clientFactory(widget, /** @type TerminalOptions */ options); - this.logger.log('create client with clientFactory', client); - } + const client = await this.clientFactory2(widget, /** @type ICreateTerminalOptions */ options); + this.logger.log('create client with clientFactory2', client); return this.setupClient(widget, client); } @@ -212,8 +204,8 @@ export class TerminalController extends WithEventBus implements ITerminalControl this.logger.log(`setup client ${client.id}`); client.addDispose( client.onExit((e) => { - // 直接使用removeWidget会导致TerminalTask场景下任务执行完毕直接退出而不是用户手动触发onKeyDown退出 - // this.terminalView.removeWidget(client.id); + // 在这个函数内不要 removeWidget,会导致 TerminalTask 场景下任务执行完毕直接退出而不是用户手动触发 onKeyDown 退出 + this._onDidCloseTerminal.fire({ id: client.id, code: e.code }); }), ); @@ -549,7 +541,7 @@ export class TerminalController extends WithEventBus implements ITerminalControl return; } - if (client?.options?.isExtensionTerminal || client?.options?.isTransient) { + if (client.launchConfig?.isExtensionOwnedTerminal || client.launchConfig?.disablePersistence) { return; } @@ -575,18 +567,6 @@ export class TerminalController extends WithEventBus implements ITerminalControl return this._clients.get(widgetId); } - /** - * @deprecated 请使用 `createClientWithWidget2`. Will removed in 2.17.0 - */ - async createClientWithWidget(options: TerminalOptions) { - return await this.createClientWithWidget2({ - terminalOptions: options, - args: options.args, - beforeCreate: options.beforeCreate, - closeWhenExited: options.closeWhenExited, - }); - } - public convertTerminalOptionsToLaunchConfig(options: TerminalOptions): IShellLaunchConfig { const shellLaunchConfig: IShellLaunchConfig = { name: options.name, @@ -610,11 +590,57 @@ export class TerminalController extends WithEventBus implements ITerminalControl shellLaunchConfig.__fromTerminalOptions = options; return shellLaunchConfig; } + convertProfileToLaunchConfig( + shellLaunchConfigOrProfile: IShellLaunchConfig | ITerminalProfile | undefined, + cwd?: Uri | string, + ): IShellLaunchConfig { + if (!shellLaunchConfigOrProfile) { + return {}; + } + // Profile was provided + if (shellLaunchConfigOrProfile && 'profileName' in shellLaunchConfigOrProfile) { + const profile = shellLaunchConfigOrProfile; + if (!profile.path) { + return shellLaunchConfigOrProfile; + } + return { + executable: profile.path, + args: profile.args, + env: profile.env, + // icon: profile.icon, + color: profile.color, + name: profile.overrideName ? profile.profileName : undefined, + cwd, + }; + } - async createClientWithWidget2(options: ICreateClientWithWidgetOptions) { - const widgetId = options.id ? this.clientId + TERMINAL_ID_SEPARATOR + options.id : this.service.generateSessionId(); + if (cwd) { + shellLaunchConfigOrProfile.cwd = cwd; + } + return shellLaunchConfigOrProfile; + } + async createTerminalWithWidgetByTerminalOptions(options: ICreateClientWithWidgetOptions) { const launchConfig = this.convertTerminalOptionsToLaunchConfig(options.terminalOptions); + return this.createTerminalWithWidget({ + ...options, + options: launchConfig, + }); + } + + async createTerminal(options: ICreateTerminalOptions) { + const widgetId = options.id || this.service.generateSessionId(); + const { group } = this._createOneGroup(); + const widget = this.terminalView.createWidget(group, widgetId, false, true); + + const client = await this._createClient(widget, options); + + return client; + } + async createTerminalWithWidget(options: ICreateTerminalWithWidgetOptions) { + const widgetId = options.id ? this.clientId + TERMINAL_ID_SEPARATOR + options.id : this.service.generateSessionId(); + + const launchConfig = this.convertProfileToLaunchConfig(options.options); const { group } = this._createOneGroup(launchConfig); const widget = this.terminalView.createWidget( @@ -631,6 +657,9 @@ export class TerminalController extends WithEventBus implements ITerminalControl const client = await this._createClient(widget, { id: widgetId, config: launchConfig, + cwd: options.options?.cwd, + location: options.options?.location, + resource: options.options?.resource, }); if (options.isTaskExecutor) { @@ -640,16 +669,6 @@ export class TerminalController extends WithEventBus implements ITerminalControl return client; } - async createTerminal(options: ICreateTerminalOptions) { - const widgetId = options.id || this.service.generateSessionId(); - const { group } = this._createOneGroup(); - const widget = this.terminalView.createWidget(group, widgetId, false, true); - - const client = await this._createClient(widget, options); - - return client; - } - clearCurrentGroup() { this.terminalView.currentGroup && this.terminalView.currentGroup.widgets.forEach((widget) => { diff --git a/packages/terminal-next/src/browser/terminal.internal.service.ts b/packages/terminal-next/src/browser/terminal.internal.service.ts index 371dacbe71..f1e4f55706 100644 --- a/packages/terminal-next/src/browser/terminal.internal.service.ts +++ b/packages/terminal-next/src/browser/terminal.internal.service.ts @@ -12,7 +12,6 @@ import { IShellLaunchConfig, ITerminalConnection, IPtyProcessChangeEvent, - TERMINAL_ID_SEPARATOR, } from '../common'; import { IXTerm } from '../common/xterm'; diff --git a/packages/terminal-next/src/browser/terminal.service.ts b/packages/terminal-next/src/browser/terminal.service.ts index 84282c6be0..dd070de03c 100644 --- a/packages/terminal-next/src/browser/terminal.service.ts +++ b/packages/terminal-next/src/browser/terminal.service.ts @@ -112,7 +112,7 @@ export class NodePtyTerminalService implements ITerminalService { launchConfig.executable = await this.getDefaultSystemShell(); } - this.logger.log(`attachByLaunchConfig ${sessionId} with launchConfig `, launchConfig); + this.logger.log(`attach terminal ${sessionId} with launchConfig `, launchConfig); const ptyInstance = await this.serviceClientRPC.create2(sessionId, cols, rows, launchConfig); if (ptyInstance && (ptyInstance.pid || ptyInstance.name)) { diff --git a/packages/terminal-next/src/browser/terminal.view.ts b/packages/terminal-next/src/browser/terminal.view.ts index a972f24536..5f44c66400 100644 --- a/packages/terminal-next/src/browser/terminal.view.ts +++ b/packages/terminal-next/src/browser/terminal.view.ts @@ -162,7 +162,7 @@ export class WidgetGroup extends Disposable implements IWidgetGroup { @computed get snapshot() { - return this.current?.name! || this.processName || this.name; + return this.current?.name || this.processName || this.name; } @computed diff --git a/packages/terminal-next/src/browser/xterm.ts b/packages/terminal-next/src/browser/xterm.ts index 42928351cb..41268ed4f0 100644 --- a/packages/terminal-next/src/browser/xterm.ts +++ b/packages/terminal-next/src/browser/xterm.ts @@ -80,7 +80,7 @@ export class XTerm extends Disposable implements IXTerm { updateTheme(theme: ITheme | undefined) { if (theme) { - this.raw.setOption('theme', theme); + this.raw.options.theme = theme; this.xtermOptions = { ...this.xtermOptions, theme, diff --git a/packages/terminal-next/src/common/client.ts b/packages/terminal-next/src/common/client.ts index 34da98d349..fda0d77f12 100644 --- a/packages/terminal-next/src/common/client.ts +++ b/packages/terminal-next/src/common/client.ts @@ -2,7 +2,7 @@ import { Terminal } from 'xterm'; import { IDisposable, Disposable, Event, Deferred } from '@opensumi/ide-core-common'; -import { INodePtyInstance, TerminalOptions, ICreateTerminalOptions } from './pty'; +import { INodePtyInstance, TerminalOptions, ICreateTerminalOptions, IShellLaunchConfig } from './pty'; import { IWidget } from './resize'; export interface ITerminalDataEvent { @@ -37,11 +37,7 @@ export interface ITerminalClient extends Disposable { */ name: string; - /** - * 终端客户端创建所使用的后端选项 - */ - options: TerminalOptions; - + launchConfig: IShellLaunchConfig; /** * 终端客户端渲染所使用的上层 dom 节点 */ @@ -138,9 +134,13 @@ export interface ITerminalClient extends Disposable { /** * 更新终端客户端配置 + * @deprecated 请使用 IShellLaunchConfig */ - updateOptions(options: TerminalOptions): void; - + updateTerminalName(options: TerminalOptions): void; + /** + * 更新终端客户端配置 + */ + updateLaunchConfig(launchConfig: IShellLaunchConfig): void; /** * clear 参数用于判断是否需要清理 meta 信息, * 不需要 clear 参数的时候基本为正常推出, @@ -183,13 +183,6 @@ export interface ITerminalClient extends Disposable { registerLinkProvider(provider: ITerminalExternalLinkProvider): IDisposable; } -export const ITerminalClientFactory = Symbol('ITerminalClientFactory'); -export type ITerminalClientFactory = ( - widget: IWidget, - options?: TerminalOptions, - disposable?: IDisposable, -) => Promise; - export const ITerminalClientFactory2 = Symbol('ITerminalClientFactory2'); export type ITerminalClientFactory2 = ( widget: IWidget, diff --git a/packages/terminal-next/src/common/controller.ts b/packages/terminal-next/src/common/controller.ts index 8e922c3ca1..0addad3a92 100644 --- a/packages/terminal-next/src/common/controller.ts +++ b/packages/terminal-next/src/common/controller.ts @@ -1,5 +1,5 @@ import { IContextKeyService } from '@opensumi/ide-core-browser'; -import { Event, Disposable, Deferred, IDisposable } from '@opensumi/ide-core-common'; +import { Event, Disposable, Deferred, IDisposable, Uri } from '@opensumi/ide-core-common'; // eslint-disable-next-line import/no-restricted-paths import type { ILinkHoverTargetOptions } from '../browser/links/link-manager'; @@ -11,6 +11,7 @@ import { ITerminalExternalLinkProvider, } from './client'; import { ITerminalLaunchError, ITerminalProcessExtHostProxy, IStartExtensionTerminalRequest } from './extension'; +import { ITerminalProfile } from './profile'; import { ITerminalInfo, ICreateTerminalOptions, TerminalOptions, IShellLaunchConfig } from './pty'; import { IWidgetGroup, IWidget } from './resize'; @@ -28,6 +29,35 @@ export interface IBoundSize { height: number; } +export interface ICreateTerminalWithWidgetOptions { + options: ICreateTerminalOptions; + + /** + * 插件进程传递的唯一 ID + */ + id?: string; + /** + * pty 进程退出后是否自动关闭 terminal 控件 + */ + closeWhenExited?: boolean; + + /** + * 是否为 TaskExecutor + */ + isTaskExecutor?: boolean; + + /** + * 作为 TaskExecutor 时对应的 taskId + */ + taskId?: string; + + /** + * 自定义的参数,由上层集成方自行控制 + */ + args?: any; + + beforeCreate?: (terminalId: string) => void; +} export interface ICreateClientWithWidgetOptions { terminalOptions: TerminalOptions; /** @@ -74,19 +104,19 @@ export interface ITerminalController extends Disposable { blur(): void; onContextMenu(e: React.MouseEvent): void; findClientFromWidgetId(widgetId: string): ITerminalClient | undefined; - /** - * @deprecated 请使用 `createClientWithWidget2` Will removed in 2.17.0 - */ - createClientWithWidget(options: TerminalOptions): Promise; - createClientWithWidget2(options: ICreateClientWithWidgetOptions): Promise; + createTerminalWithWidgetByTerminalOptions(options: ICreateClientWithWidgetOptions): Promise; createTerminal(options: ICreateTerminalOptions): Promise; + createTerminalWithWidget(options: ICreateTerminalWithWidgetOptions): Promise; clearCurrentGroup(): void; clearAllGroups(): void; showTerminalPanel(): void; hideTerminalPanel(): void; toJSON(): ITerminalBrowserHistory; convertTerminalOptionsToLaunchConfig(options: TerminalOptions): IShellLaunchConfig; - + convertProfileToLaunchConfig( + shellLaunchConfigOrProfile: IShellLaunchConfig | ITerminalProfile | undefined, + cwd?: Uri | string, + ): IShellLaunchConfig; onDidOpenTerminal: Event; onDidCloseTerminal: Event; onDidTerminalTitleChange: Event; diff --git a/packages/terminal-next/src/common/pty.ts b/packages/terminal-next/src/common/pty.ts index 5a89cf07ec..bb2b0829cf 100644 --- a/packages/terminal-next/src/common/pty.ts +++ b/packages/terminal-next/src/common/pty.ts @@ -10,6 +10,23 @@ import { ITerminalEnvironment, ITerminalProcessExtHostProxy, TerminalLocation } import { IDetectProfileOptions, ITerminalProfile } from './profile'; import { WindowsShellType } from './shell'; +export interface IPtySpawnOptions { + /** + * 恢复终端的历史记录的特性 + * + * 在 Task 启动终端的时候我们要关掉这个特性,因为我们期望每一次 Task 仅返回当次执行的结果 + * + * @default true + */ + preserveHistory?: boolean; + /** + * 保存的终端历史记录的行数 + * TODO: 可以通过设置项改变 + * @default 500 + */ + ptyLineCacheSize?: number; +} + export interface IPtyProcess extends INodePty { /** * @deprecated 请使用 `IPty.launchConfig` 的 shellPath 字段 @@ -44,6 +61,7 @@ export interface IPtyProxyRPCService { args: string[] | string, options: pty.IPtyForkOptions | pty.IWindowsPtyForkOptions, sessionId?: string, + spawnOptions?: IPtySpawnOptions, ): Promise; /** @@ -292,7 +310,7 @@ export interface INodePtyInstance { id: string; name: string; pid: number; - proess: string; + process: string; shellPath?: string; } @@ -534,6 +552,11 @@ export interface IShellLaunchConfig { */ disablePersistence?: boolean; + /** + * 禁用保持 Shell 历史的特性 + */ + disablePreserveHistory?: boolean; + __fromTerminalOptions?: TerminalOptions; } diff --git a/packages/terminal-next/src/common/service.ts b/packages/terminal-next/src/common/service.ts index dd2aec8789..79e90d634e 100644 --- a/packages/terminal-next/src/common/service.ts +++ b/packages/terminal-next/src/common/service.ts @@ -1,11 +1,11 @@ -import { ITerminalOptions as IXtermTerminalOptions, Terminal } from 'xterm'; +import { ITerminalOptions as IXtermTerminalOptions } from 'xterm'; import { IDisposable, OperatingSystem } from '@opensumi/ide-core-common'; import { ITerminalConnection } from './client'; import { ITerminalError } from './error'; import { ITerminalProfile } from './profile'; -import { IShellLaunchConfig, TerminalOptions } from './pty'; +import { IShellLaunchConfig } from './pty'; import { IXTerm } from './xterm'; export interface IPtyExitEvent { @@ -22,11 +22,11 @@ export interface IPtyProcessChangeEvent { export const ITerminalService = Symbol('ITerminalService'); export interface ITerminalService { /** - * 自定义 sessionId + * generate sessionId */ generateSessionId?(): string; /** - * Xterm 终端的构造选项, + * Xterm 终端的构造选项 * 默认返回为 {} */ getOptions?(): IXtermTerminalOptions; diff --git a/packages/terminal-next/src/node/pty.manager.ts b/packages/terminal-next/src/node/pty.manager.ts index e989ddd37d..3945cb2c47 100644 --- a/packages/terminal-next/src/node/pty.manager.ts +++ b/packages/terminal-next/src/node/pty.manager.ts @@ -3,7 +3,7 @@ import * as pty from 'node-pty'; import { Injectable, Autowired } from '@opensumi/di'; import { INodeLogger } from '@opensumi/ide-core-node'; -import { IPtyProcessProxy, IPtyProxyRPCService, IShellLaunchConfig } from '../common'; +import { IPtyProcessProxy, IPtyProxyRPCService, IPtySpawnOptions, IShellLaunchConfig } from '../common'; import { PtyServiceProxy } from './pty.proxy'; @@ -18,8 +18,9 @@ export interface IPtyServiceManager { spawn( file: string, args: string[] | string, - options: pty.IPtyForkOptions | pty.IWindowsPtyForkOptions, + ptyOptions: pty.IPtyForkOptions | pty.IWindowsPtyForkOptions, sessionId?: string, + spawnOptions?: IPtySpawnOptions, ): Promise; // 因为 PtyServiceManage 是 PtyClient 端统筹所有 Pty 的管理类,因此每一个具体方法的调用都需要传入 pid 来对指定 pid 做某些操作 onData(pid: number, listener: (e: string) => any): pty.IDisposable; @@ -59,7 +60,7 @@ export class PtyServiceManager implements IPtyServiceManager { } protected initLocal() { - const callback = async (callId, ...args) => { + const callback = async (callId: number, ...args) => { const callback = this.callbackMap.get(callId); if (!callback) { return Promise.reject(new Error(`no found callback: ${callId}`)); @@ -90,17 +91,24 @@ export class PtyServiceManager implements IPtyServiceManager { args: string[] | string, options: pty.IPtyForkOptions | pty.IWindowsPtyForkOptions, sessionId?: string, + spawnOptions?: IPtySpawnOptions, ): Promise { - const iPtyRemoteProxy = (await this.ptyServiceProxy.$spawn(file, args, options, sessionId)) as pty.IPty; - // 局部功能的 Ipty, 代理所有常量 - return new PtyProcessProxy(iPtyRemoteProxy, this); + const ptyRemoteProxy = (await this.ptyServiceProxy.$spawn( + file, + args, + options, + sessionId, + spawnOptions, + )) as pty.IPty; + // 局部功能的 IPty, 代理所有常量 + return new PtyProcessProxy(ptyRemoteProxy, this); } async getProcess(pid: any): Promise { return await this.ptyServiceProxy.$getProcess(pid); } - // 实现 Ipty 的需要回调的逻辑接口,同时注入 + // 实现 IPty 的需要回调的逻辑接口,同时注入 onData(pid: number, listener: (e: string) => any): pty.IDisposable { const monitorListener = (resString) => { listener(resString); @@ -161,13 +169,13 @@ export class PtyServiceManager implements IPtyServiceManager { // 实现了 IPtyProcessProxy 背后是 NodePty 的 INodePty, 因此可以做到和本地化直接调用 NodePty 的代码兼容 class PtyProcessProxy implements IPtyProcessProxy { private ptyServiceManager: IPtyServiceManager; - constructor(iptyProxy: pty.IPty, ptyServiceManager: IPtyServiceManager) { + constructor(ptyProxy: pty.IPty, ptyServiceManager: IPtyServiceManager) { this.ptyServiceManager = ptyServiceManager; - this.pid = iptyProxy.pid; - this.cols = iptyProxy.cols; - this.rows = iptyProxy.rows; - this._process = iptyProxy.process; - this.handleFlowControl = iptyProxy.handleFlowControl; + this.pid = ptyProxy.pid; + this.cols = ptyProxy.cols; + this.rows = ptyProxy.rows; + this._process = ptyProxy.process; + this.handleFlowControl = ptyProxy.handleFlowControl; this.onData = (listener: (e: string) => any) => this.ptyServiceManager.onData(this.pid, listener); this.onExit = (listener: (e: { exitCode: number; signal?: number }) => any) => diff --git a/packages/terminal-next/src/node/pty.proxy.ts b/packages/terminal-next/src/node/pty.proxy.ts index a9476aa1b6..f5766b7e67 100644 --- a/packages/terminal-next/src/node/pty.proxy.ts +++ b/packages/terminal-next/src/node/pty.proxy.ts @@ -10,18 +10,21 @@ import { DisposableCollection, getDebugLogger } from '@opensumi/ide-core-node'; import { IPtyProxyRPCService, + IPtySpawnOptions, PTY_SERVICE_PROXY_CALLBACK_PROTOCOL, PTY_SERVICE_PROXY_PROTOCOL, PTY_SERVICE_PROXY_SERVER_PORT, TERMINAL_ID_SEPARATOR, } from '../common'; +const PTY_LINE_DATA_CACHE_DEFAULT_SIZE = 500; + // 存储Pty-onData返回的数据行,用于用户Resume场景下的数据恢复 class PtyLineDataCache { private size: number; private dataArray: string[] = []; - constructor(size = 100) { + constructor(size = PTY_LINE_DATA_CACHE_DEFAULT_SIZE) { this.size = size; } @@ -92,6 +95,7 @@ export class PtyServiceProxy implements IPtyProxyRPCService { args: string[] | string, options: pty.IPtyForkOptions | pty.IWindowsPtyForkOptions, longSessionId?: string, + spawnOptions?: IPtySpawnOptions, ): any { // 切割sessionId到短Id const sessionId = longSessionId?.split(TERMINAL_ID_SEPARATOR)?.[1]; @@ -109,11 +113,13 @@ export class PtyServiceProxy implements IPtyProxyRPCService { // 有Session ID 但是没有 Process,说明是被系统杀了,此时需要重新spawn一个Pty this.ptyInstanceMap.delete(pid); ptyInstance = pty.spawn(file, args, options); - // 这种情况下,需要把之前的PtyCache给attach上去,方便用户查看记录 - const oldLineCache = this.ptyDataCacheMap.get(pid); - if (oldLineCache) { - this.ptyDataCacheMap.set(ptyInstance.pid, oldLineCache); - this.ptyDataCacheMap.delete(pid); + if (spawnOptions?.preserveHistory) { + // 这种情况下,需要把之前的PtyCache给attach上去,方便用户查看记录 + const oldLineCache = this.ptyDataCacheMap.get(pid); + if (oldLineCache) { + this.ptyDataCacheMap.set(ptyInstance.pid, oldLineCache); + this.ptyDataCacheMap.delete(pid); + } } } } @@ -129,9 +135,10 @@ export class PtyServiceProxy implements IPtyProxyRPCService { } const pid = ptyInstance.pid; + const cacheSize = spawnOptions?.ptyLineCacheSize ?? PTY_LINE_DATA_CACHE_DEFAULT_SIZE; // 初始化PtyLineCache if (!this.ptyDataCacheMap.has(pid)) { - this.ptyDataCacheMap.set(pid, new PtyLineDataCache()); + this.ptyDataCacheMap.set(pid, new PtyLineDataCache(cacheSize)); } // 初始化ptyDisposableMap if (!this.ptyDisposableMap.has(pid)) { diff --git a/packages/terminal-next/src/node/pty.ts b/packages/terminal-next/src/node/pty.ts index ccbca297a5..374c8f4ce6 100644 --- a/packages/terminal-next/src/node/pty.ts +++ b/packages/terminal-next/src/node/pty.ts @@ -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 } from '../common/pty'; +import { IPtyProcess, IPtySpawnOptions } from '../common/pty'; import { IPtyServiceManager, PtyServiceManagerToken } from './pty.manager'; import { findExecutable } from './shell'; @@ -150,7 +150,7 @@ export class PtyService extends Disposable { async start(): Promise { const options = this.shellLaunchConfig; - const locale = osLocale.sync(); + const locale = await osLocale.default(); let ptyEnv: { [key: string]: string }; if (options.strictEnv) { @@ -201,13 +201,16 @@ export class PtyService extends Disposable { protected async setupPtyProcess() { const options = this.shellLaunchConfig; - + const ptySpawnOptions: IPtySpawnOptions = { + preserveHistory: !options?.disablePreserveHistory, + }; const args = options.args || []; const ptyProcess = await this.ptyServiceManager.spawn( options.executable as string, args, this._ptyOptions, this.sessionId, + ptySpawnOptions, ); this.addDispose( diff --git a/packages/terminal-next/src/node/terminal.service.client.ts b/packages/terminal-next/src/node/terminal.service.client.ts index bbdd80114c..26969dd5c5 100644 --- a/packages/terminal-next/src/node/terminal.service.client.ts +++ b/packages/terminal-next/src/node/terminal.service.client.ts @@ -13,7 +13,7 @@ import { IDetectProfileOptions, ITerminalProfile } from '../common/profile'; import { IPtyProcess } from '../common/pty'; import { WindowsShellType, WINDOWS_DEFAULT_SHELL_PATH_MAPS } from '../common/shell'; -import { findExecutable, findShellExecutableAsync, getSystemShell, WINDOWS_GIT_BASH_PATHS } from './shell'; +import { findShellExecutableAsync, getSystemShell, WINDOWS_GIT_BASH_PATHS } from './shell'; import { ITerminalProfileServiceNode, TerminalProfileServiceNode } from './terminal.profile.service'; /** @@ -96,7 +96,7 @@ export class TerminalServiceClientImpl extends RPCService i return { id, pid: pty.pid, - proess: pty.process, + process: pty.process, name: pty.parsedName, shellPath: pty.launchConfig.executable, }; diff --git a/packages/terminal-next/src/node/terminal.service.ts b/packages/terminal-next/src/node/terminal.service.ts index e484814d73..108fc99164 100644 --- a/packages/terminal-next/src/node/terminal.service.ts +++ b/packages/terminal-next/src/node/terminal.service.ts @@ -99,19 +99,27 @@ export class TerminalServiceImpl implements ITerminalNodeService { t.shellLaunchConfig.isExtensionOwnedTerminal || isElectronNodeEnv ) { - t.kill(); // terminalProfile有isTransient的参数化,要Kill,不保活 + t.kill(); // shellLaunchConfig 有 isTransient 的参数时,要Kill,不保活 } // t.kill(); // 这个是窗口关闭时候触发,终端默认在这种场景下保活, 不kill - // TOOD 后续看看有没有更加优雅的方案 + // TODO: 后续看看有没有更加优雅的方案 }); this.clientTerminalMap.delete(clientId); } } private flushPtyData(clientId: string, sessionId: string) { + if (!this.batchedPtyDataMap.has(sessionId)) { + return; + } + const ptyData = this.batchedPtyDataMap.get(sessionId)!; this.batchedPtyDataMap.delete(sessionId); - this.batchedPtyDataTimer.delete(sessionId); + + if (this.batchedPtyDataTimer.has(sessionId)) { + global.clearTimeout(this.batchedPtyDataTimer.get(sessionId)!); + this.batchedPtyDataTimer.delete(sessionId); + } const serviceClient = this.serviceClientMap.get(clientId) as ITerminalServiceClient; serviceClient.clientMessage(sessionId, ptyData); @@ -125,12 +133,12 @@ export class TerminalServiceImpl implements ITerminalNodeService { options: IShellLaunchConfig, ): Promise; public async create2( - id: string, + sessionId: string, _cols: unknown, _rows?: unknown, _launchConfig?: unknown, ): Promise { - const clientId = id.split(TERMINAL_ID_SEPARATOR)[0]; + const clientId = sessionId.split(TERMINAL_ID_SEPARATOR)[0]; let ptyService: PtyService | undefined; let cols = _cols as number; let rows = _rows as number; @@ -145,8 +153,8 @@ export class TerminalServiceImpl implements ITerminalNodeService { } try { - ptyService = this.injector.get(PtyService, [id, launchConfig, cols, rows]); - this.terminalProcessMap.set(id, ptyService); + ptyService = this.injector.get(PtyService, [sessionId, launchConfig, cols, rows]); + this.terminalProcessMap.set(sessionId, ptyService); // ref: https://hyper.is/blog // 合并 pty 输出的数据,16ms 后发送给客户端,如 @@ -155,27 +163,22 @@ export class TerminalServiceImpl implements ITerminalNodeService { // 存的数据,避免因为输出较多时阻塞 RPC 通信 ptyService.onData((chunk: string) => { if (this.serviceClientMap.has(clientId)) { - if (!this.batchedPtyDataMap.has(id)) { - this.batchedPtyDataMap.set(id, ''); + if (!this.batchedPtyDataMap.has(sessionId)) { + this.batchedPtyDataMap.set(sessionId, ''); } - this.batchedPtyDataMap.set(id, this.batchedPtyDataMap.get(id) + chunk); + this.batchedPtyDataMap.set(sessionId, this.batchedPtyDataMap.get(sessionId) + chunk); - const ptyData = this.batchedPtyDataMap.get(id) || ''; + const ptyData = this.batchedPtyDataMap.get(sessionId) || ''; if (ptyData?.length + chunk.length >= BATCH_MAX_SIZE) { - if (this.batchedPtyDataTimer.has(id)) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - global.clearTimeout(this.batchedPtyDataTimer.get(id)!); - this.batchedPtyDataTimer.delete(id); - } - this.flushPtyData(clientId, id); + this.flushPtyData(clientId, sessionId); } - if (!this.batchedPtyDataTimer.has(id)) { + if (!this.batchedPtyDataTimer.has(sessionId)) { this.batchedPtyDataTimer.set( - id, - global.setTimeout(() => this.flushPtyData(clientId, id), BATCH_DURATION_MS), + sessionId, + global.setTimeout(() => this.flushPtyData(clientId, sessionId), BATCH_DURATION_MS), ); } } else { @@ -184,10 +187,11 @@ export class TerminalServiceImpl implements ITerminalNodeService { }); ptyService.onExit(({ exitCode, signal }) => { - this.logger.debug(`Terminal process exit (instanceId: ${id}) with code ${exitCode}`); + this.logger.debug(`Terminal process exit (instanceId: ${sessionId}) with code ${exitCode}`); if (this.serviceClientMap.has(clientId)) { + this.flushPtyData(clientId, sessionId); const serviceClient = this.serviceClientMap.get(clientId) as ITerminalServiceClient; - serviceClient.closeClient(id, { + serviceClient.closeClient(sessionId, { code: exitCode, signal, }); @@ -200,7 +204,7 @@ export class TerminalServiceImpl implements ITerminalNodeService { this.logger.debug(`Terminal process change (${processName})`); if (this.serviceClientMap.has(clientId)) { const serviceClient = this.serviceClientMap.get(clientId) as ITerminalServiceClient; - serviceClient.processChange(id, processName); + serviceClient.processChange(sessionId, processName); } else { this.logger.warn(`terminal: pty ${clientId} on data not found`); } @@ -208,22 +212,22 @@ export class TerminalServiceImpl implements ITerminalNodeService { const error = await ptyService.start(); if (error) { - this.logger.error(`Terminal process start error (instanceId: ${id})`, error); + this.logger.error(`Terminal process start error (instanceId: ${sessionId})`, error); throw error; } if (!this.clientTerminalMap.has(clientId)) { this.clientTerminalMap.set(clientId, new Map()); } - this.clientTerminalMap.get(clientId)?.set(id, ptyService); + this.clientTerminalMap.get(clientId)?.set(sessionId, ptyService); } catch (error) { this.logger.error( - `${id} create terminal error: ${JSON.stringify(error)}, options: ${JSON.stringify(launchConfig)}`, + `${sessionId} create terminal error: ${JSON.stringify(error)}, options: ${JSON.stringify(launchConfig)}`, ); if (this.serviceClientMap.has(clientId)) { const serviceClient = this.serviceClientMap.get(clientId) as ITerminalServiceClient; - serviceClient.closeClient(id, { - id, + serviceClient.closeClient(sessionId, { + id: sessionId, message: error?.message, type: ETerminalErrorType.CREATE_FAIL, stopped: true,