@theia/terminal 1.73.0-next.9 → 1.73.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/browser/base/terminal-service.d.ts +1 -1
- package/lib/browser/base/terminal-service.d.ts.map +1 -1
- package/lib/browser/base/terminal-widget.d.ts +6 -0
- package/lib/browser/base/terminal-widget.d.ts.map +1 -1
- package/lib/browser/base/terminal-widget.js.map +1 -1
- package/lib/browser/index.d.ts +1 -0
- package/lib/browser/index.d.ts.map +1 -1
- package/lib/browser/index.js +1 -0
- package/lib/browser/index.js.map +1 -1
- package/lib/browser/search/terminal-search-widget.js +2 -2
- package/lib/browser/search/terminal-search-widget.js.map +1 -1
- package/lib/browser/terminal-creation-handler.d.ts +35 -0
- package/lib/browser/terminal-creation-handler.d.ts.map +1 -0
- package/lib/browser/terminal-creation-handler.js +33 -0
- package/lib/browser/terminal-creation-handler.js.map +1 -0
- package/lib/browser/terminal-file-link-provider.d.ts +2 -1
- package/lib/browser/terminal-file-link-provider.d.ts.map +1 -1
- package/lib/browser/terminal-file-link-provider.js +7 -2
- package/lib/browser/terminal-file-link-provider.js.map +1 -1
- package/lib/browser/terminal-frontend-contribution.d.ts +10 -1
- package/lib/browser/terminal-frontend-contribution.d.ts.map +1 -1
- package/lib/browser/terminal-frontend-contribution.js +76 -53
- package/lib/browser/terminal-frontend-contribution.js.map +1 -1
- package/lib/browser/terminal-frontend-module.d.ts.map +1 -1
- package/lib/browser/terminal-frontend-module.js +5 -0
- package/lib/browser/terminal-frontend-module.js.map +1 -1
- package/lib/browser/terminal-shell-handler.d.ts +19 -0
- package/lib/browser/terminal-shell-handler.d.ts.map +1 -0
- package/lib/browser/terminal-shell-handler.js +95 -0
- package/lib/browser/terminal-shell-handler.js.map +1 -0
- package/lib/browser/terminal-widget-impl.d.ts +21 -0
- package/lib/browser/terminal-widget-impl.d.ts.map +1 -1
- package/lib/browser/terminal-widget-impl.js +59 -7
- package/lib/browser/terminal-widget-impl.js.map +1 -1
- package/lib/node/buffering-stream.d.ts +2 -0
- package/lib/node/buffering-stream.d.ts.map +1 -1
- package/lib/node/buffering-stream.js +12 -0
- package/lib/node/buffering-stream.js.map +1 -1
- package/lib/node/buffering-stream.spec.js +27 -0
- package/lib/node/buffering-stream.spec.js.map +1 -1
- package/lib/node/shell-terminal-server.d.ts +7 -0
- package/lib/node/shell-terminal-server.d.ts.map +1 -1
- package/lib/node/shell-terminal-server.js +69 -0
- package/lib/node/shell-terminal-server.js.map +1 -1
- package/lib/node/shell-terminal-server.spec.js +99 -0
- package/lib/node/shell-terminal-server.spec.js.map +1 -1
- package/lib/node/terminal-backend-contribution.d.ts.map +1 -1
- package/lib/node/terminal-backend-contribution.js +5 -0
- package/lib/node/terminal-backend-contribution.js.map +1 -1
- package/package.json +10 -10
- package/src/browser/base/terminal-service.ts +1 -1
- package/src/browser/base/terminal-widget.ts +7 -0
- package/src/browser/index.ts +1 -0
- package/src/browser/search/terminal-search-widget.tsx +2 -2
- package/src/browser/terminal-creation-handler.ts +53 -0
- package/src/browser/terminal-file-link-provider.ts +6 -4
- package/src/browser/terminal-frontend-contribution.ts +79 -57
- package/src/browser/terminal-frontend-module.ts +5 -0
- package/src/browser/terminal-shell-handler.ts +89 -0
- package/src/browser/terminal-widget-impl.ts +64 -8
- package/src/node/buffering-stream.spec.ts +30 -0
- package/src/node/buffering-stream.ts +13 -0
- package/src/node/shell-terminal-server.spec.ts +119 -1
- package/src/node/shell-terminal-server.ts +70 -0
- package/src/node/terminal-backend-contribution.ts +5 -0
|
@@ -40,6 +40,36 @@ describe('BufferringStream', () => {
|
|
|
40
40
|
expect(await waitForChunk(buffer)).deep.eq(Buffer.from([4, 5]));
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
+
it('should flush with buffer bigger than maxChunkSize', () => {
|
|
44
|
+
const buffer = new BufferBufferingStream({ maxChunkSize: 2 });
|
|
45
|
+
const chunks: Buffer[] = [];
|
|
46
|
+
buffer.onData(c => chunks.push(c));
|
|
47
|
+
buffer.push(Buffer.from([0, 1, 2, 3, 4, 5]));
|
|
48
|
+
buffer.flush();
|
|
49
|
+
expect(chunks).deep.eq([Buffer.from([0, 1]), Buffer.from([2, 3]), Buffer.from([4, 5])]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should flush with empty buffer', () => {
|
|
53
|
+
const buffer = new BufferBufferingStream({ maxChunkSize: 2 });
|
|
54
|
+
const chunks: Buffer[] = [];
|
|
55
|
+
buffer.onData(c => chunks.push(c));
|
|
56
|
+
buffer.flush();
|
|
57
|
+
expect(chunks).to.be.empty;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should not let push during a flush affect the result of flush', async () => {
|
|
61
|
+
const buffer = new BufferBufferingStream({ maxChunkSize: 2 });
|
|
62
|
+
const chunks: Buffer[] = [];
|
|
63
|
+
buffer.onData(c => {
|
|
64
|
+
chunks.push(c);
|
|
65
|
+
buffer.push(Buffer.from([99]));
|
|
66
|
+
});
|
|
67
|
+
buffer.push(Buffer.from([0, 1, 2, 3]));
|
|
68
|
+
buffer.flush();
|
|
69
|
+
expect(chunks).deep.eq([Buffer.from([0, 1]), Buffer.from([2, 3])]);
|
|
70
|
+
expect(await waitForChunk(buffer)).deep.eq(Buffer.from([99, 99]));
|
|
71
|
+
});
|
|
72
|
+
|
|
43
73
|
function waitForChunk(buffer: BufferBufferingStream): Promise<Buffer> {
|
|
44
74
|
return new Promise(resolve => buffer.onData(resolve));
|
|
45
75
|
}
|
|
@@ -65,6 +65,19 @@ export class BufferingStream<T> {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/** Immediately drains all buffered data, cancelling any pending timed flush. */
|
|
69
|
+
flush(): void {
|
|
70
|
+
if (this.buffer !== undefined) {
|
|
71
|
+
clearTimeout(this.timeout);
|
|
72
|
+
const snapshot = this.buffer;
|
|
73
|
+
this.buffer = undefined;
|
|
74
|
+
const totalLength = this.length(snapshot);
|
|
75
|
+
for (let offset = 0; offset < totalLength; offset += this.maxChunkSize) {
|
|
76
|
+
this.onDataEmitter.fire(this.slice(snapshot, offset, offset + this.maxChunkSize));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
68
81
|
dispose(): void {
|
|
69
82
|
clearTimeout(this.timeout);
|
|
70
83
|
this.buffer = undefined;
|
|
@@ -14,8 +14,11 @@
|
|
|
14
14
|
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
import * as chai from 'chai';
|
|
17
|
+
import * as os from 'os';
|
|
18
|
+
import * as path from 'path';
|
|
19
|
+
import { FileUri } from '@theia/core/lib/common/file-uri';
|
|
17
20
|
import { createTerminalTestContainer } from './test/terminal-test-container';
|
|
18
|
-
import { IShellTerminalServer } from '../common/shell-terminal-protocol';
|
|
21
|
+
import { IShellTerminalServer, IShellTerminalServerOptions } from '../common/shell-terminal-protocol';
|
|
19
22
|
|
|
20
23
|
/**
|
|
21
24
|
* Globals
|
|
@@ -37,4 +40,119 @@ describe('ShellServer', function (): void {
|
|
|
37
40
|
|
|
38
41
|
expect(await createResult).to.be.greaterThan(-1);
|
|
39
42
|
});
|
|
43
|
+
|
|
44
|
+
describe('validateTerminalOptions', () => {
|
|
45
|
+
|
|
46
|
+
// Access the protected method for direct testing
|
|
47
|
+
function validate(options: IShellTerminalServerOptions): IShellTerminalServerOptions {
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
|
+
(shellTerminalServer as any).validateTerminalOptions(options);
|
|
50
|
+
return options;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
it('should accept valid options unchanged', () => {
|
|
54
|
+
const options = validate({ cols: 80, rows: 24 });
|
|
55
|
+
expect(options.cols).to.equal(80);
|
|
56
|
+
expect(options.rows).to.equal(24);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should reset cols/rows outside valid range', () => {
|
|
60
|
+
const options = validate({ cols: -1, rows: 9999 });
|
|
61
|
+
expect(options.cols).to.be.undefined;
|
|
62
|
+
expect(options.rows).to.be.undefined;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should reset non-integer cols/rows', () => {
|
|
66
|
+
const options = validate({ cols: 80.5, rows: NaN });
|
|
67
|
+
expect(options.cols).to.be.undefined;
|
|
68
|
+
expect(options.rows).to.be.undefined;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should accept valid string args', () => {
|
|
72
|
+
const options = validate({ args: ['-l', '--color'] });
|
|
73
|
+
expect(options.args).to.deep.equal(['-l', '--color']);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should accept a single string as args', () => {
|
|
77
|
+
const options = validate({ args: '-l' });
|
|
78
|
+
expect(options.args).to.equal('-l');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should reset args with non-string elements', () => {
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
83
|
+
const options = validate({ args: ['-l', 42 as any] });
|
|
84
|
+
expect(options.args).to.be.undefined;
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should reset non-array non-string args', () => {
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
89
|
+
const options = validate({ args: { cmd: 'evil' } as any });
|
|
90
|
+
expect(options.args).to.be.undefined;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should accept rootURI pointing to an existing directory', () => {
|
|
94
|
+
const uri = FileUri.create(os.tmpdir()).toString();
|
|
95
|
+
const options = validate({ rootURI: uri });
|
|
96
|
+
expect(options.rootURI).to.equal(uri);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should reset rootURI pointing to a non-existent path', () => {
|
|
100
|
+
const options = validate({ rootURI: 'file:///nonexistent/path/xyz123' });
|
|
101
|
+
expect(options.rootURI).to.be.undefined;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should reset rootURI pointing to a file instead of directory', () => {
|
|
105
|
+
const filePath = path.join(os.tmpdir(), 'theia-test-' + Date.now());
|
|
106
|
+
require('fs').writeFileSync(filePath, 'test');
|
|
107
|
+
try {
|
|
108
|
+
const options = validate({ rootURI: FileUri.create(filePath).toString() });
|
|
109
|
+
expect(options.rootURI).to.be.undefined;
|
|
110
|
+
} finally {
|
|
111
|
+
require('fs').unlinkSync(filePath);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should reset shell that does not exist', () => {
|
|
116
|
+
const options = validate({ shell: '/nonexistent/shell' });
|
|
117
|
+
expect(options.shell).to.be.undefined;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should accept a valid shell', () => {
|
|
121
|
+
// process.execPath is the Node binary used to run the tests, so it always exists
|
|
122
|
+
// and is executable on every platform - making it a reliable cross-platform shell value.
|
|
123
|
+
const options = validate({ shell: process.execPath });
|
|
124
|
+
expect(options.shell).to.equal(process.execPath);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should reset non-string shell type', () => {
|
|
128
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
129
|
+
const options = validate({ shell: 42 as any });
|
|
130
|
+
expect(options.shell).to.be.undefined;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should reset malformed rootURI', () => {
|
|
134
|
+
const options = validate({ rootURI: 'not-a-valid-uri' });
|
|
135
|
+
expect(options.rootURI).to.be.undefined;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should reject cols: 0 (below minimum)', () => {
|
|
139
|
+
const options = validate({ cols: 0 });
|
|
140
|
+
expect(options.cols).to.be.undefined;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should accept cols: 500 (upper boundary)', () => {
|
|
144
|
+
const options = validate({ cols: 500 });
|
|
145
|
+
expect(options.cols).to.equal(500);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should reject cols: 501 (above maximum)', () => {
|
|
149
|
+
const options = validate({ cols: 501 });
|
|
150
|
+
expect(options.cols).to.be.undefined;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should accept rows: 1 (lower boundary)', () => {
|
|
154
|
+
const options = validate({ rows: 1 });
|
|
155
|
+
expect(options.rows).to.equal(1);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
40
158
|
});
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
import { exec } from 'child_process';
|
|
17
|
+
import * as fs from 'fs';
|
|
17
18
|
|
|
18
19
|
import { inject, injectable, named } from '@theia/core/shared/inversify';
|
|
19
20
|
import { ILogger } from '@theia/core/lib/common/logger';
|
|
@@ -58,6 +59,7 @@ export class ShellTerminalServer extends BaseTerminalServer implements IShellTer
|
|
|
58
59
|
|
|
59
60
|
async create(options: IShellTerminalServerOptions): Promise<number> {
|
|
60
61
|
try {
|
|
62
|
+
this.validateTerminalOptions(options);
|
|
61
63
|
if (options.strictEnv !== true) {
|
|
62
64
|
options.env = this.environmentUtils.mergeProcessEnv(options.env);
|
|
63
65
|
this.applyToProcessEnvironment(URI.fromFilePath(getRootPath(options.rootURI)), options.env);
|
|
@@ -71,6 +73,74 @@ export class ShellTerminalServer extends BaseTerminalServer implements IShellTer
|
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Validates and sanitizes terminal options received from the client.
|
|
78
|
+
* Invalid values are replaced with safe defaults rather than rejecting the request,
|
|
79
|
+
* so legitimate users with misconfigured preferences still get a working terminal.
|
|
80
|
+
*/
|
|
81
|
+
protected validateTerminalOptions(options: IShellTerminalServerOptions): void {
|
|
82
|
+
// Validate shell: must be an existing, executable file. Fall back to default otherwise.
|
|
83
|
+
if (options.shell !== undefined) {
|
|
84
|
+
if (typeof options.shell !== 'string' || !this.isValidShell(options.shell)) {
|
|
85
|
+
this.logger.warn(`Invalid shell "${options.shell}": falling back to default shell.`);
|
|
86
|
+
options.shell = undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Validate rootURI: must resolve to an existing directory.
|
|
91
|
+
if (options.rootURI !== undefined) {
|
|
92
|
+
try {
|
|
93
|
+
const rootPath = getRootPath(options.rootURI);
|
|
94
|
+
if (!fs.existsSync(rootPath) || !fs.statSync(rootPath).isDirectory()) {
|
|
95
|
+
this.logger.warn(`Invalid rootURI "${options.rootURI}": falling back to home directory.`);
|
|
96
|
+
options.rootURI = undefined;
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
this.logger.warn(`Invalid rootURI "${options.rootURI}": falling back to home directory.`);
|
|
100
|
+
options.rootURI = undefined;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Validate cols/rows: must be positive integers within reasonable bounds.
|
|
105
|
+
if (options.cols !== undefined) {
|
|
106
|
+
if (typeof options.cols !== 'number' || !Number.isInteger(options.cols) || options.cols < 1 || options.cols > 500) {
|
|
107
|
+
options.cols = undefined;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (options.rows !== undefined) {
|
|
111
|
+
if (typeof options.rows !== 'number' || !Number.isInteger(options.rows) || options.rows < 1 || options.rows > 500) {
|
|
112
|
+
options.rows = undefined;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Validate args: must be a string array or a single string.
|
|
117
|
+
if (options.args !== undefined) {
|
|
118
|
+
if (typeof options.args === 'string') {
|
|
119
|
+
// single string is fine
|
|
120
|
+
} else if (Array.isArray(options.args)) {
|
|
121
|
+
if (!options.args.every(arg => typeof arg === 'string')) {
|
|
122
|
+
options.args = undefined;
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
options.args = undefined;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
protected isValidShell(shell: string): boolean {
|
|
131
|
+
try {
|
|
132
|
+
// On Windows, cmd.exe and powershell.exe are resolved from PATH,
|
|
133
|
+
// so we only check accessibility for absolute paths.
|
|
134
|
+
if (isWindows && !/[/\\]/.test(shell)) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
fs.accessSync(shell, fs.constants.X_OK);
|
|
138
|
+
return true;
|
|
139
|
+
} catch {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
74
144
|
// copied and modified from https://github.com/microsoft/vscode/blob/4636be2b71c87bfb0bfe3c94278b447a5efcc1f1/src/vs/workbench/contrib/debug/node/terminals.ts#L32-L75
|
|
75
145
|
private spawnAsPromised(command: string, args: string[]): Promise<string> {
|
|
76
146
|
return new Promise((resolve, reject) => {
|
|
@@ -50,7 +50,12 @@ export class TerminalBackendContribution implements MessagingService.Contributio
|
|
|
50
50
|
output.on('data', chunk => {
|
|
51
51
|
buffer.push(chunk);
|
|
52
52
|
});
|
|
53
|
+
const toDisposeOnProcessClose = termProcess.onClose(() => {
|
|
54
|
+
buffer.flush();
|
|
55
|
+
channel.close();
|
|
56
|
+
});
|
|
53
57
|
channel.onClose(() => {
|
|
58
|
+
toDisposeOnProcessClose.dispose();
|
|
54
59
|
buffer.dispose();
|
|
55
60
|
output.dispose();
|
|
56
61
|
});
|