Skip to content

Commit 887037b

Browse files
authored
Merge pull request microsoft#250672 from microsoft/copilot/fix-250671
Add workbench.action.terminal.sendSignal command
2 parents b2a80a8 + abc0765 commit 887037b

20 files changed

+154
-1
lines changed

src/vs/platform/terminal/common/terminal.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ export interface IPtyService {
330330
start(id: number): Promise<ITerminalLaunchError | { injectedArgs: string[] } | undefined>;
331331
shutdown(id: number, immediate: boolean): Promise<void>;
332332
input(id: number, data: string): Promise<void>;
333+
sendSignal(id: number, signal: string): Promise<void>;
333334
resize(id: number, cols: number, rows: number): Promise<void>;
334335
clearBuffer(id: number): Promise<void>;
335336
getInitialCwd(id: number): Promise<string>;
@@ -786,6 +787,7 @@ export interface ITerminalChildProcess {
786787
*/
787788
shutdown(immediate: boolean): void;
788789
input(data: string): void;
790+
sendSignal(signal: string): void;
789791
processBinary(data: string): Promise<void>;
790792
resize(cols: number, rows: number): void;
791793
clearBuffer(): void | Promise<void>;

src/vs/platform/terminal/node/ptyHostService.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,9 @@ export class PtyHostService extends Disposable implements IPtyHostService {
248248
input(id: number, data: string): Promise<void> {
249249
return this._proxy.input(id, data);
250250
}
251+
sendSignal(id: number, signal: string): Promise<void> {
252+
return this._proxy.sendSignal(id, signal);
253+
}
251254
processBinary(id: number, data: string): Promise<void> {
252255
return this._proxy.processBinary(id, data);
253256
}

src/vs/platform/terminal/node/ptyService.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,10 @@ export class PtyService extends Disposable implements IPtyService {
422422
}
423423
}
424424
@traceRpc
425+
async sendSignal(id: number, signal: string): Promise<void> {
426+
return this._throwIfNoPty(id).sendSignal(signal);
427+
}
428+
@traceRpc
425429
async processBinary(id: number, data: string): Promise<void> {
426430
return this._throwIfNoPty(id).writeBinary(data);
427431
}
@@ -856,6 +860,12 @@ class PersistentTerminalProcess extends Disposable {
856860
}
857861
return this._terminalProcess.input(data);
858862
}
863+
sendSignal(signal: string): void {
864+
if (this._inReplay) {
865+
return;
866+
}
867+
return this._terminalProcess.sendSignal(signal);
868+
}
859869
writeBinary(data: string): Promise<void> {
860870
return this._terminalProcess.processBinary(data);
861871
}

src/vs/platform/terminal/node/terminalProcess.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,13 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
469469
this._startWrite();
470470
}
471471

472+
sendSignal(signal: string): void {
473+
if (this._store.isDisposed || !this._ptyProcess) {
474+
return;
475+
}
476+
this._ptyProcess.kill(signal);
477+
}
478+
472479
async processBinary(data: string): Promise<void> {
473480
this.input(data, true);
474481
}

src/vs/server/node/remoteTerminalChannel.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ export class RemoteTerminalChannel extends Disposable implements IServerChannel<
128128

129129
case RemoteTerminalChannelRequest.Start: return this._ptyHostService.start.apply(this._ptyHostService, args);
130130
case RemoteTerminalChannelRequest.Input: return this._ptyHostService.input.apply(this._ptyHostService, args);
131+
case RemoteTerminalChannelRequest.SendSignal: return this._ptyHostService.sendSignal.apply(this._ptyHostService, args);
131132
case RemoteTerminalChannelRequest.AcknowledgeDataEvent: return this._ptyHostService.acknowledgeDataEvent.apply(this._ptyHostService, args);
132133
case RemoteTerminalChannelRequest.Shutdown: return this._ptyHostService.shutdown.apply(this._ptyHostService, args);
133134
case RemoteTerminalChannelRequest.Resize: return this._ptyHostService.resize.apply(this._ptyHostService, args);

src/vs/workbench/api/common/extHostTerminalService.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,11 @@ class ExtHostPseudoterminal implements ITerminalChildProcess {
334334
this._pty.handleInput?.(data);
335335
}
336336

337+
sendSignal(signal: string): void {
338+
// Extension owned terminals don't support sending signals directly to processes
339+
// This could be extended in the future if the pseudoterminal API is enhanced
340+
}
341+
337342
resize(cols: number, rows: number): void {
338343
this._pty.setDimensions?.({ columns: cols, rows });
339344
}

src/vs/workbench/contrib/terminal/browser/remotePty.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,16 @@ export class RemotePty extends BasePty implements ITerminalChildProcess {
6565
});
6666
}
6767

68+
sendSignal(signal: string): void {
69+
if (this._inReplay) {
70+
return;
71+
}
72+
73+
this._startBarrier.wait().then(_ => {
74+
this._remoteTerminalChannel.sendSignal(this.id, signal);
75+
});
76+
}
77+
6878
processBinary(e: string): Promise<void> {
6979
return this._remoteTerminalChannel.processBinary(this.id, e);
7080
}

src/vs/workbench/contrib/terminal/browser/terminal.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,13 @@ export interface ITerminalInstance extends IBaseTerminalInstance {
899899
*/
900900
sendText(text: string, shouldExecute: boolean, bracketedPasteMode?: boolean): Promise<void>;
901901

902+
/**
903+
* Sends a signal to the terminal instance's process.
904+
*
905+
* @param signal The signal to send (e.g., 'SIGTERM', 'SIGINT', 'SIGKILL').
906+
*/
907+
sendSignal(signal: string): Promise<void>;
908+
902909
/**
903910
* Sends a path to the terminal instance, preparing it as needed based on the detected shell
904911
* running within the terminal. The text is written to the stdin of the underlying pty process

src/vs/workbench/contrib/terminal/browser/terminalActions.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { ILabelService } from '../../../../platform/label/common/label.js';
2525
import { IListService } from '../../../../platform/list/browser/listService.js';
2626
import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';
2727
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
28-
import { IPickOptions, IQuickInputService, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js';
28+
import { IPickOptions, IQuickInputService, IQuickPickItem, QuickPickItem } from '../../../../platform/quickinput/common/quickInput.js';
2929
import { ITerminalProfile, TerminalExitReason, TerminalIcon, TerminalLocation, TerminalSettingId } from '../../../../platform/terminal/common/terminal.js';
3030
import { IWorkspaceContextService, IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js';
3131
import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from '../../../browser/actions/workspaceCommands.js';
@@ -166,6 +166,56 @@ export const terminalSendSequenceCommand = async (accessor: ServicesAccessor, ar
166166
instance.sendText(resolvedText, false);
167167
};
168168

169+
export const terminalSendSignalCommand = async (accessor: ServicesAccessor, args: unknown) => {
170+
const quickInputService = accessor.get(IQuickInputService);
171+
const instance = accessor.get(ITerminalService).activeInstance;
172+
if (!instance) {
173+
return;
174+
}
175+
176+
let signal = isObject(args) && 'signal' in args ? toOptionalString(args.signal) : undefined;
177+
178+
if (!signal) {
179+
const signalOptions: QuickPickItem[] = [
180+
{ label: 'SIGINT', description: localize('SIGINT', 'Interrupt process (Ctrl+C)') },
181+
{ label: 'SIGTERM', description: localize('SIGTERM', 'Terminate process gracefully') },
182+
{ label: 'SIGKILL', description: localize('SIGKILL', 'Force kill process') },
183+
{ label: 'SIGSTOP', description: localize('SIGSTOP', 'Stop process') },
184+
{ label: 'SIGCONT', description: localize('SIGCONT', 'Continue process') },
185+
{ label: 'SIGHUP', description: localize('SIGHUP', 'Hangup') },
186+
{ label: 'SIGQUIT', description: localize('SIGQUIT', 'Quit process') },
187+
{ label: 'SIGUSR1', description: localize('SIGUSR1', 'User-defined signal 1') },
188+
{ label: 'SIGUSR2', description: localize('SIGUSR2', 'User-defined signal 2') },
189+
{ type: 'separator' },
190+
{ label: localize('manualSignal', 'Manually enter signal') }
191+
];
192+
193+
const selected = await quickInputService.pick(signalOptions, {
194+
placeHolder: localize('selectSignal', 'Select signal to send to terminal process')
195+
});
196+
197+
if (!selected) {
198+
return;
199+
}
200+
201+
if (selected.label === localize('manualSignal', 'Manually enter signal')) {
202+
const inputSignal = await quickInputService.input({
203+
prompt: localize('enterSignal', 'Enter signal name (e.g., SIGTERM, SIGKILL)'),
204+
});
205+
206+
if (!inputSignal) {
207+
return;
208+
}
209+
210+
signal = inputSignal;
211+
} else {
212+
signal = selected.label;
213+
}
214+
}
215+
216+
await instance.sendSignal(signal);
217+
};
218+
169219
export class TerminalLaunchHelpAction extends Action {
170220

171221
constructor(
@@ -1005,6 +1055,29 @@ export function registerTerminalActions() {
10051055
run: (c, accessor, args) => terminalSendSequenceCommand(accessor, args)
10061056
});
10071057

1058+
registerTerminalAction({
1059+
id: TerminalCommandId.SendSignal,
1060+
title: terminalStrings.sendSignal,
1061+
f1: !isWindows,
1062+
metadata: {
1063+
description: terminalStrings.sendSignal.value,
1064+
args: [{
1065+
name: 'args',
1066+
schema: {
1067+
type: 'object',
1068+
required: ['signal'],
1069+
properties: {
1070+
signal: {
1071+
description: localize('sendSignal', "The signal to send to the terminal process (e.g., 'SIGTERM', 'SIGINT', 'SIGKILL')"),
1072+
type: 'string'
1073+
}
1074+
},
1075+
}
1076+
}]
1077+
},
1078+
run: (c, accessor, args) => terminalSendSignalCommand(accessor, args)
1079+
});
1080+
10081081
registerTerminalAction({
10091082
id: TerminalCommandId.NewWithCwd,
10101083
title: terminalStrings.newWithCwd,

src/vs/workbench/contrib/terminal/browser/terminalInstance.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1317,6 +1317,11 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
13171317
}
13181318
}
13191319

1320+
async sendSignal(signal: string): Promise<void> {
1321+
this._logService.debug('sending signal (vscode)', signal);
1322+
await this._processManager.sendSignal(signal);
1323+
}
1324+
13201325
async sendPath(originalPath: string | URI, shouldExecute: boolean): Promise<void> {
13211326
return this.sendText(await this.preparePathForShell(originalPath), shouldExecute);
13221327
}

0 commit comments

Comments
 (0)