@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.
Files changed (65) hide show
  1. package/lib/browser/base/terminal-service.d.ts +1 -1
  2. package/lib/browser/base/terminal-service.d.ts.map +1 -1
  3. package/lib/browser/base/terminal-widget.d.ts +6 -0
  4. package/lib/browser/base/terminal-widget.d.ts.map +1 -1
  5. package/lib/browser/base/terminal-widget.js.map +1 -1
  6. package/lib/browser/index.d.ts +1 -0
  7. package/lib/browser/index.d.ts.map +1 -1
  8. package/lib/browser/index.js +1 -0
  9. package/lib/browser/index.js.map +1 -1
  10. package/lib/browser/search/terminal-search-widget.js +2 -2
  11. package/lib/browser/search/terminal-search-widget.js.map +1 -1
  12. package/lib/browser/terminal-creation-handler.d.ts +35 -0
  13. package/lib/browser/terminal-creation-handler.d.ts.map +1 -0
  14. package/lib/browser/terminal-creation-handler.js +33 -0
  15. package/lib/browser/terminal-creation-handler.js.map +1 -0
  16. package/lib/browser/terminal-file-link-provider.d.ts +2 -1
  17. package/lib/browser/terminal-file-link-provider.d.ts.map +1 -1
  18. package/lib/browser/terminal-file-link-provider.js +7 -2
  19. package/lib/browser/terminal-file-link-provider.js.map +1 -1
  20. package/lib/browser/terminal-frontend-contribution.d.ts +10 -1
  21. package/lib/browser/terminal-frontend-contribution.d.ts.map +1 -1
  22. package/lib/browser/terminal-frontend-contribution.js +76 -53
  23. package/lib/browser/terminal-frontend-contribution.js.map +1 -1
  24. package/lib/browser/terminal-frontend-module.d.ts.map +1 -1
  25. package/lib/browser/terminal-frontend-module.js +5 -0
  26. package/lib/browser/terminal-frontend-module.js.map +1 -1
  27. package/lib/browser/terminal-shell-handler.d.ts +19 -0
  28. package/lib/browser/terminal-shell-handler.d.ts.map +1 -0
  29. package/lib/browser/terminal-shell-handler.js +95 -0
  30. package/lib/browser/terminal-shell-handler.js.map +1 -0
  31. package/lib/browser/terminal-widget-impl.d.ts +21 -0
  32. package/lib/browser/terminal-widget-impl.d.ts.map +1 -1
  33. package/lib/browser/terminal-widget-impl.js +59 -7
  34. package/lib/browser/terminal-widget-impl.js.map +1 -1
  35. package/lib/node/buffering-stream.d.ts +2 -0
  36. package/lib/node/buffering-stream.d.ts.map +1 -1
  37. package/lib/node/buffering-stream.js +12 -0
  38. package/lib/node/buffering-stream.js.map +1 -1
  39. package/lib/node/buffering-stream.spec.js +27 -0
  40. package/lib/node/buffering-stream.spec.js.map +1 -1
  41. package/lib/node/shell-terminal-server.d.ts +7 -0
  42. package/lib/node/shell-terminal-server.d.ts.map +1 -1
  43. package/lib/node/shell-terminal-server.js +69 -0
  44. package/lib/node/shell-terminal-server.js.map +1 -1
  45. package/lib/node/shell-terminal-server.spec.js +99 -0
  46. package/lib/node/shell-terminal-server.spec.js.map +1 -1
  47. package/lib/node/terminal-backend-contribution.d.ts.map +1 -1
  48. package/lib/node/terminal-backend-contribution.js +5 -0
  49. package/lib/node/terminal-backend-contribution.js.map +1 -1
  50. package/package.json +10 -10
  51. package/src/browser/base/terminal-service.ts +1 -1
  52. package/src/browser/base/terminal-widget.ts +7 -0
  53. package/src/browser/index.ts +1 -0
  54. package/src/browser/search/terminal-search-widget.tsx +2 -2
  55. package/src/browser/terminal-creation-handler.ts +53 -0
  56. package/src/browser/terminal-file-link-provider.ts +6 -4
  57. package/src/browser/terminal-frontend-contribution.ts +79 -57
  58. package/src/browser/terminal-frontend-module.ts +5 -0
  59. package/src/browser/terminal-shell-handler.ts +89 -0
  60. package/src/browser/terminal-widget-impl.ts +64 -8
  61. package/src/node/buffering-stream.spec.ts +30 -0
  62. package/src/node/buffering-stream.ts +13 -0
  63. package/src/node/shell-terminal-server.spec.ts +119 -1
  64. package/src/node/shell-terminal-server.ts +70 -0
  65. 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
  });