@theia/core 1.26.0 → 1.27.0-next.10

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 (164) hide show
  1. package/README.md +6 -7
  2. package/lib/browser/messaging/ws-connection-provider.d.ts +5 -4
  3. package/lib/browser/messaging/ws-connection-provider.d.ts.map +1 -1
  4. package/lib/browser/messaging/ws-connection-provider.js +30 -23
  5. package/lib/browser/messaging/ws-connection-provider.js.map +1 -1
  6. package/lib/browser/progress-status-bar-item.d.ts +1 -1
  7. package/lib/browser/progress-status-bar-item.d.ts.map +1 -1
  8. package/lib/browser/tree/tree-compression/compressed-tree-widget.js +2 -2
  9. package/lib/browser/tree/tree-compression/compressed-tree-widget.js.map +1 -1
  10. package/lib/browser/widgets/select-component.d.ts +4 -1
  11. package/lib/browser/widgets/select-component.d.ts.map +1 -1
  12. package/lib/browser/widgets/select-component.js +30 -16
  13. package/lib/browser/widgets/select-component.js.map +1 -1
  14. package/lib/common/cancellation.d.ts +1 -0
  15. package/lib/common/cancellation.d.ts.map +1 -1
  16. package/lib/common/cancellation.js +8 -0
  17. package/lib/common/cancellation.js.map +1 -1
  18. package/lib/common/index.d.ts +2 -0
  19. package/lib/common/index.d.ts.map +1 -1
  20. package/lib/common/index.js +2 -0
  21. package/lib/common/index.js.map +1 -1
  22. package/lib/common/message-rpc/channel.d.ts +106 -0
  23. package/lib/common/message-rpc/channel.d.ts.map +1 -0
  24. package/lib/common/message-rpc/channel.js +195 -0
  25. package/lib/common/message-rpc/channel.js.map +1 -0
  26. package/lib/common/message-rpc/channel.spec.d.ts +9 -0
  27. package/lib/common/message-rpc/channel.spec.d.ts.map +1 -0
  28. package/lib/common/message-rpc/channel.spec.js +80 -0
  29. package/lib/common/message-rpc/channel.spec.js.map +1 -0
  30. package/lib/common/message-rpc/index.d.ts +4 -0
  31. package/lib/common/message-rpc/index.d.ts.map +1 -0
  32. package/lib/{node/messaging/logger.js → common/message-rpc/index.js} +6 -19
  33. package/lib/common/message-rpc/index.js.map +1 -0
  34. package/lib/common/message-rpc/message-buffer.d.ts +50 -0
  35. package/lib/common/message-rpc/message-buffer.d.ts.map +1 -0
  36. package/lib/common/message-rpc/message-buffer.js +56 -0
  37. package/lib/common/message-rpc/message-buffer.js.map +1 -0
  38. package/lib/common/message-rpc/rpc-message-encoder.d.ts +159 -0
  39. package/lib/common/message-rpc/rpc-message-encoder.d.ts.map +1 -0
  40. package/lib/common/message-rpc/rpc-message-encoder.js +362 -0
  41. package/lib/common/message-rpc/rpc-message-encoder.js.map +1 -0
  42. package/lib/common/message-rpc/rpc-message-encoder.spec.d.ts +2 -0
  43. package/lib/common/message-rpc/rpc-message-encoder.spec.d.ts.map +1 -0
  44. package/lib/common/message-rpc/rpc-message-encoder.spec.js +37 -0
  45. package/lib/common/message-rpc/rpc-message-encoder.spec.js.map +1 -0
  46. package/lib/common/message-rpc/rpc-protocol.d.ts +61 -0
  47. package/lib/common/message-rpc/rpc-protocol.d.ts.map +1 -0
  48. package/lib/common/message-rpc/rpc-protocol.js +183 -0
  49. package/lib/common/message-rpc/rpc-protocol.js.map +1 -0
  50. package/lib/common/message-rpc/uint8-array-message-buffer.d.ts +52 -0
  51. package/lib/common/message-rpc/uint8-array-message-buffer.d.ts.map +1 -0
  52. package/lib/common/message-rpc/uint8-array-message-buffer.js +169 -0
  53. package/lib/common/message-rpc/uint8-array-message-buffer.js.map +1 -0
  54. package/lib/common/message-rpc/uint8-array-message-buffer.spec.d.ts +2 -0
  55. package/lib/common/message-rpc/uint8-array-message-buffer.spec.d.ts.map +1 -0
  56. package/lib/common/message-rpc/uint8-array-message-buffer.spec.js +39 -0
  57. package/lib/common/message-rpc/uint8-array-message-buffer.spec.js.map +1 -0
  58. package/lib/common/messaging/abstract-connection-provider.d.ts +9 -8
  59. package/lib/common/messaging/abstract-connection-provider.d.ts.map +1 -1
  60. package/lib/common/messaging/abstract-connection-provider.js +20 -35
  61. package/lib/common/messaging/abstract-connection-provider.js.map +1 -1
  62. package/lib/common/messaging/connection-error-handler.d.ts +1 -2
  63. package/lib/common/messaging/connection-error-handler.d.ts.map +1 -1
  64. package/lib/common/messaging/connection-error-handler.js +1 -1
  65. package/lib/common/messaging/connection-error-handler.js.map +1 -1
  66. package/lib/common/messaging/handler.d.ts +2 -2
  67. package/lib/common/messaging/handler.d.ts.map +1 -1
  68. package/lib/common/messaging/proxy-factory.d.ts +13 -7
  69. package/lib/common/messaging/proxy-factory.d.ts.map +1 -1
  70. package/lib/common/messaging/proxy-factory.js +18 -13
  71. package/lib/common/messaging/proxy-factory.js.map +1 -1
  72. package/lib/common/messaging/proxy-factory.spec.js +4 -15
  73. package/lib/common/messaging/proxy-factory.spec.js.map +1 -1
  74. package/lib/common/messaging/web-socket-channel.d.ts +54 -48
  75. package/lib/common/messaging/web-socket-channel.d.ts.map +1 -1
  76. package/lib/common/messaging/web-socket-channel.js +41 -105
  77. package/lib/common/messaging/web-socket-channel.js.map +1 -1
  78. package/lib/electron-browser/messaging/electron-ipc-connection-provider.d.ts +2 -2
  79. package/lib/electron-browser/messaging/electron-ipc-connection-provider.d.ts.map +1 -1
  80. package/lib/electron-browser/messaging/electron-ipc-connection-provider.js +18 -7
  81. package/lib/electron-browser/messaging/electron-ipc-connection-provider.js.map +1 -1
  82. package/lib/electron-browser/messaging/electron-ws-connection-provider.d.ts +2 -2
  83. package/lib/electron-browser/messaging/electron-ws-connection-provider.d.ts.map +1 -1
  84. package/lib/electron-browser/messaging/electron-ws-connection-provider.js +5 -7
  85. package/lib/electron-browser/messaging/electron-ws-connection-provider.js.map +1 -1
  86. package/lib/electron-main/messaging/electron-messaging-contribution.d.ts +37 -9
  87. package/lib/electron-main/messaging/electron-messaging-contribution.d.ts.map +1 -1
  88. package/lib/electron-main/messaging/electron-messaging-contribution.js +83 -68
  89. package/lib/electron-main/messaging/electron-messaging-contribution.js.map +1 -1
  90. package/lib/electron-main/messaging/electron-messaging-service.d.ts +2 -8
  91. package/lib/electron-main/messaging/electron-messaging-service.d.ts.map +1 -1
  92. package/lib/electron-main/messaging/electron-messaging-service.js.map +1 -1
  93. package/lib/electron-main/theia-electron-window.d.ts.map +1 -1
  94. package/lib/electron-main/theia-electron-window.js +11 -8
  95. package/lib/electron-main/theia-electron-window.js.map +1 -1
  96. package/lib/node/messaging/binary-message-pipe.d.ts +45 -0
  97. package/lib/node/messaging/binary-message-pipe.d.ts.map +1 -0
  98. package/lib/node/messaging/binary-message-pipe.js +152 -0
  99. package/lib/node/messaging/binary-message-pipe.js.map +1 -0
  100. package/lib/node/messaging/ipc-bootstrap.js +2 -11
  101. package/lib/node/messaging/ipc-bootstrap.js.map +1 -1
  102. package/lib/node/messaging/ipc-channel.d.ts +26 -0
  103. package/lib/node/messaging/ipc-channel.d.ts.map +1 -0
  104. package/lib/node/messaging/ipc-channel.js +86 -0
  105. package/lib/node/messaging/ipc-channel.js.map +1 -0
  106. package/lib/node/messaging/ipc-connection-provider.d.ts +3 -5
  107. package/lib/node/messaging/ipc-connection-provider.d.ts.map +1 -1
  108. package/lib/node/messaging/ipc-connection-provider.js +14 -31
  109. package/lib/node/messaging/ipc-connection-provider.js.map +1 -1
  110. package/lib/node/messaging/ipc-protocol.d.ts +2 -2
  111. package/lib/node/messaging/ipc-protocol.d.ts.map +1 -1
  112. package/lib/node/messaging/messaging-contribution.d.ts +6 -9
  113. package/lib/node/messaging/messaging-contribution.d.ts.map +1 -1
  114. package/lib/node/messaging/messaging-contribution.js +23 -68
  115. package/lib/node/messaging/messaging-contribution.js.map +1 -1
  116. package/lib/node/messaging/messaging-service.d.ts +4 -23
  117. package/lib/node/messaging/messaging-service.d.ts.map +1 -1
  118. package/lib/node/messaging/messaging-service.js +1 -15
  119. package/lib/node/messaging/messaging-service.js.map +1 -1
  120. package/lib/node/messaging/test/test-web-socket-channel.d.ts +4 -2
  121. package/lib/node/messaging/test/test-web-socket-channel.d.ts.map +1 -1
  122. package/lib/node/messaging/test/test-web-socket-channel.js +25 -12
  123. package/lib/node/messaging/test/test-web-socket-channel.js.map +1 -1
  124. package/package.json +5 -8
  125. package/src/browser/messaging/ws-connection-provider.ts +34 -25
  126. package/src/browser/progress-status-bar-item.ts +1 -1
  127. package/src/browser/style/menus.css +1 -0
  128. package/src/browser/tree/tree-compression/compressed-tree-widget.tsx +2 -2
  129. package/src/browser/widgets/select-component.tsx +37 -17
  130. package/src/common/cancellation.ts +8 -0
  131. package/src/common/index.ts +2 -0
  132. package/src/common/message-rpc/channel.spec.ts +88 -0
  133. package/src/common/message-rpc/channel.ts +260 -0
  134. package/src/{node/messaging/logger.ts → common/message-rpc/index.ts} +4 -23
  135. package/src/common/message-rpc/message-buffer.ts +99 -0
  136. package/src/common/message-rpc/rpc-message-encoder.spec.ts +42 -0
  137. package/src/common/message-rpc/rpc-message-encoder.ts +497 -0
  138. package/src/common/message-rpc/rpc-protocol.ts +217 -0
  139. package/src/common/message-rpc/uint8-array-message-buffer.spec.ts +41 -0
  140. package/src/common/message-rpc/uint8-array-message-buffer.ts +206 -0
  141. package/src/common/messaging/abstract-connection-provider.ts +28 -37
  142. package/src/common/messaging/connection-error-handler.ts +1 -2
  143. package/src/common/messaging/handler.ts +2 -2
  144. package/src/common/messaging/proxy-factory.spec.ts +4 -17
  145. package/src/common/messaging/proxy-factory.ts +27 -16
  146. package/src/common/messaging/web-socket-channel.ts +79 -135
  147. package/src/electron-browser/messaging/electron-ipc-connection-provider.ts +21 -7
  148. package/src/electron-browser/messaging/electron-ws-connection-provider.ts +5 -8
  149. package/src/electron-main/messaging/electron-messaging-contribution.ts +87 -65
  150. package/src/electron-main/messaging/electron-messaging-service.ts +2 -8
  151. package/src/electron-main/theia-electron-window.ts +12 -9
  152. package/src/node/messaging/binary-message-pipe.ts +168 -0
  153. package/src/node/messaging/ipc-bootstrap.ts +3 -11
  154. package/src/node/messaging/ipc-channel.ts +97 -0
  155. package/src/node/messaging/ipc-connection-provider.ts +18 -35
  156. package/src/node/messaging/ipc-protocol.ts +2 -2
  157. package/src/node/messaging/messaging-contribution.ts +29 -74
  158. package/src/node/messaging/messaging-service.ts +4 -31
  159. package/src/node/messaging/test/test-web-socket-channel.ts +26 -17
  160. package/lib/node/messaging/logger.d.ts +0 -8
  161. package/lib/node/messaging/logger.d.ts.map +0 -1
  162. package/lib/node/messaging/logger.js.map +0 -1
  163. package/shared/vscode-ws-jsonrpc/index.d.ts +0 -1
  164. package/shared/vscode-ws-jsonrpc/index.js +0 -1
@@ -20,8 +20,10 @@ import { APPLICATION_STATE_CHANGE_SIGNAL, CLOSE_REQUESTED_SIGNAL, RELOAD_REQUEST
20
20
  import { BrowserWindow, BrowserWindowConstructorOptions, ipcMain, IpcMainEvent } from '../../electron-shared/electron';
21
21
  import { inject, injectable, postConstruct } from '../../shared/inversify';
22
22
  import { ElectronMainApplicationGlobals } from './electron-main-constants';
23
- import { DisposableCollection, Emitter, Event, isWindows } from '../common';
23
+ import { DisposableCollection, Emitter, Event } from '../common';
24
24
  import { createDisposableListener } from './event-utils';
25
+ import { URI } from '../common/uri';
26
+ import { FileUri } from '../node/file-uri';
25
27
 
26
28
  /**
27
29
  * Theia tracks the maximized state of Electron Browser Windows.
@@ -114,14 +116,15 @@ export class TheiaElectronWindow {
114
116
  }
115
117
 
116
118
  protected async handleStopRequest(onSafeCallback: () => unknown, reason: StopReason): Promise<boolean> {
117
- // Only confirm close to windows that have loaded our front end.
118
- let currentUrl = this.window.webContents.getURL();
119
- let frontendUri = this.globals.THEIA_FRONTEND_HTML_PATH;
120
- // Since our resolved frontend HTML path might contain backward slashes on Windows, we normalize everything first.
121
- if (isWindows) {
122
- currentUrl = currentUrl.replace(/\\/g, '/');
123
- frontendUri = frontendUri.replace(/\\/g, '/');
124
- }
119
+ // Only confirm close to windows that have loaded our frontend.
120
+ // Both the windows's URL and the FS path of the `index.html` should be converted to the "same" format to be able to compare them. (#11226)
121
+ // Notes:
122
+ // - Windows: file:///C:/path/to/somewhere vs file:///c%3A/path/to/somewhere
123
+ // - macOS: file:///Applications/App%20Name.app/Contents vs /Applications/App Name.app/Contents
124
+ // This URL string comes from electron, we can expect that this is properly encoded URL. For example, a space is `%20`
125
+ const currentUrl = new URI(this.window.webContents.getURL()).toString();
126
+ // THEIA_FRONTEND_HTML_PATH is an FS path, we have to covert to an encoded URI string.
127
+ const frontendUri = FileUri.create(this.globals.THEIA_FRONTEND_HTML_PATH).toString();
125
128
  const safeToClose = !currentUrl.includes(frontendUri) || await this.checkSafeToStop(reason);
126
129
  if (safeToClose) {
127
130
  try {
@@ -0,0 +1,168 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2022 STMicroelectronics and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { Duplex } from 'stream';
18
+ import { Disposable, Emitter, Event } from '../../common';
19
+ import { Uint8ArrayReadBuffer, Uint8ArrayWriteBuffer } from '../../common/message-rpc/uint8-array-message-buffer';
20
+
21
+ /**
22
+ * A `BinaryMessagePipe` is capable of sending and retrieving binary messages i.e. {@link Uint8Array}s over
23
+ * and underlying streamed process pipe/fd. The message length of individual messages is encoding at the beginning of
24
+ * a new message. This makes it possible to extract messages from the streamed data.
25
+ */
26
+ export class BinaryMessagePipe implements Disposable {
27
+ static readonly MESSAGE_START_IDENTIFIER = '<MessageStart>';
28
+ protected dataHandler = (chunk: Uint8Array) => this.handleChunk(chunk);
29
+
30
+ protected onMessageEmitter = new Emitter<Uint8Array>();
31
+ protected cachedMessageData: StreamedMessageData = {
32
+ chunks: [],
33
+ missingBytes: 0
34
+ };
35
+
36
+ get onMessage(): Event<Uint8Array> {
37
+ return this.onMessageEmitter.event;
38
+ }
39
+
40
+ constructor(protected readonly underlyingPipe: Duplex) {
41
+ underlyingPipe.on('data', this.dataHandler);
42
+ }
43
+
44
+ send(message: Uint8Array): void {
45
+ this.underlyingPipe.write(this.encodeMessageStart(message));
46
+ this.underlyingPipe.write(message);
47
+ }
48
+
49
+ protected handleChunk(chunk: Uint8Array): void {
50
+ if (this.cachedMessageData.missingBytes === 0) {
51
+ // There is no currently streamed message => We expect that the beginning of the chunk is the message start for a new message
52
+ this.handleNewMessage(chunk);
53
+ } else {
54
+ // The chunk contains message data intended for the currently cached message
55
+ this.handleMessageContentChunk(chunk);
56
+ }
57
+ }
58
+
59
+ protected handleNewMessage(chunk: Uint8Array): void {
60
+ if (chunk.byteLength < this.messageStartByteLength) {
61
+ // The chunk only contains a part of the encoded message start
62
+ this.cachedMessageData.partialMessageStart = chunk;
63
+ return;
64
+ }
65
+
66
+ const messageLength = this.readMessageStart(chunk);
67
+
68
+ if (chunk.length - this.messageStartByteLength > messageLength) {
69
+ // The initial chunk contains more than one binary message => Fire `onMessage` for first message and handle remaining content
70
+ const firstMessage = chunk.slice(this.messageStartByteLength, this.messageStartByteLength + messageLength);
71
+ this.onMessageEmitter.fire(firstMessage);
72
+ this.handleNewMessage(chunk.slice(this.messageStartByteLength + messageLength));
73
+
74
+ } else if (chunk.length - this.messageStartByteLength === messageLength) {
75
+ // The initial chunk contains exactly one complete message. => Directly fire the `onMessage` event.
76
+ this.onMessageEmitter.fire(chunk.slice(this.messageStartByteLength));
77
+ } else {
78
+ // The initial chunk contains only part of the message content => Cache message data
79
+ this.cachedMessageData.chunks = [chunk.slice(this.messageStartByteLength)];
80
+ this.cachedMessageData.missingBytes = messageLength - chunk.byteLength + this.messageStartByteLength;
81
+ }
82
+ }
83
+
84
+ protected handleMessageContentChunk(chunk: Uint8Array): void {
85
+ if (this.cachedMessageData) {
86
+ if (chunk.byteLength < this.cachedMessageData.missingBytes) {
87
+ // The chunk only contains parts of the missing bytes for the cached message.
88
+ this.cachedMessageData.chunks.push(chunk);
89
+ this.cachedMessageData.missingBytes -= chunk.byteLength;
90
+ } else if (chunk.byteLength === this.cachedMessageData.missingBytes) {
91
+ // Chunk contains exactly the missing data for the cached message
92
+ this.cachedMessageData.chunks.push(chunk);
93
+ this.emitCachedMessage();
94
+ } else {
95
+ // Chunk contains missing data for the cached message + data for the next message
96
+ const messageEnd = this.cachedMessageData.missingBytes;
97
+ const missingData = chunk.slice(0, messageEnd);
98
+ this.cachedMessageData.chunks.push(missingData);
99
+ this.emitCachedMessage();
100
+ this.handleNewMessage(chunk.slice(messageEnd));
101
+ }
102
+
103
+ }
104
+ }
105
+
106
+ protected emitCachedMessage(): void {
107
+ const message = Buffer.concat(this.cachedMessageData.chunks);
108
+ this.onMessageEmitter.fire(message);
109
+ this.cachedMessageData.chunks = [];
110
+ this.cachedMessageData.missingBytes = 0;
111
+ }
112
+
113
+ /**
114
+ * Encodes the start of a new message into a {@link Uint8Array}.
115
+ * The message start consists of a identifier string and the length of the following message.
116
+ * @returns the buffer contains the encoded message start
117
+ */
118
+ protected encodeMessageStart(message: Uint8Array): Uint8Array {
119
+ const writer = new Uint8ArrayWriteBuffer()
120
+ .writeString(BinaryMessagePipe.MESSAGE_START_IDENTIFIER)
121
+ .writeUint32(message.length);
122
+ const messageStart = writer.getCurrentContents();
123
+ writer.dispose();
124
+ return messageStart;
125
+ }
126
+
127
+ protected get messageStartByteLength(): number {
128
+ // 4 bytes for length of id + id string length + 4 bytes for length of message
129
+ return 4 + BinaryMessagePipe.MESSAGE_START_IDENTIFIER.length + 4;
130
+ }
131
+
132
+ /**
133
+ * Reads the start of a new message from a stream chunk (or cached message) received from the underlying pipe.
134
+ * The message start is expected to consist of an identifier string and the length of the message.
135
+ * @param chunk The stream chunk.
136
+ * @returns The length of the message content to read.
137
+ * @throws An error if the message start can not be read successfully.
138
+ */
139
+ protected readMessageStart(chunk: Uint8Array): number {
140
+ const messageData = this.cachedMessageData.partialMessageStart ? Buffer.concat([this.cachedMessageData.partialMessageStart, chunk]) : chunk;
141
+ this.cachedMessageData.partialMessageStart = undefined;
142
+
143
+ const reader = new Uint8ArrayReadBuffer(messageData);
144
+ const identifier = reader.readString();
145
+
146
+ if (identifier !== BinaryMessagePipe.MESSAGE_START_IDENTIFIER) {
147
+ throw new Error(`Could not read message start. The start identifier should be '${BinaryMessagePipe.MESSAGE_START_IDENTIFIER}' but was '${identifier}`);
148
+ }
149
+ const length = reader.readUint32();
150
+ return length;
151
+ }
152
+
153
+ dispose(): void {
154
+ this.underlyingPipe.removeListener('data', this.dataHandler);
155
+ this.underlyingPipe.end();
156
+ this.onMessageEmitter.dispose();
157
+ this.cachedMessageData = {
158
+ chunks: [],
159
+ missingBytes: 0
160
+ };
161
+ }
162
+ }
163
+
164
+ interface StreamedMessageData {
165
+ chunks: Uint8Array[];
166
+ missingBytes: number;
167
+ partialMessageStart?: Uint8Array;
168
+ }
@@ -16,20 +16,12 @@
16
16
 
17
17
  import 'reflect-metadata';
18
18
  import { dynamicRequire } from '../dynamic-require';
19
- import { ConsoleLogger } from 'vscode-ws-jsonrpc/lib/logger';
20
- import { createMessageConnection, IPCMessageReader, IPCMessageWriter, Trace } from 'vscode-ws-jsonrpc';
19
+ import { IPCChannel } from './ipc-channel';
21
20
  import { checkParentAlive, IPCEntryPoint } from './ipc-protocol';
22
21
 
23
22
  checkParentAlive();
24
23
 
25
24
  const entryPoint = IPCEntryPoint.getScriptFromEnv();
26
- const reader = new IPCMessageReader(process);
27
- const writer = new IPCMessageWriter(process);
28
- const logger = new ConsoleLogger();
29
- const connection = createMessageConnection(reader, writer, logger);
30
- connection.trace(Trace.Off, {
31
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
- log: (message: any, data?: string) => console.log(message, data)
33
- });
34
25
 
35
- dynamicRequire<{ default: IPCEntryPoint }>(entryPoint).default(connection);
26
+ dynamicRequire<{ default: IPCEntryPoint }>(entryPoint).default(new IPCChannel());
27
+
@@ -0,0 +1,97 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2022 STMicroelectronics and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+ /* eslint-disable @typescript-eslint/no-explicit-any */
17
+
18
+ import * as cp from 'child_process';
19
+ import { Socket } from 'net';
20
+ import { Duplex } from 'stream';
21
+ import { Channel, ChannelCloseEvent, Disposable, DisposableCollection, Emitter, Event, MessageProvider, WriteBuffer } from '../../common';
22
+ import { Uint8ArrayReadBuffer, Uint8ArrayWriteBuffer } from '../../common/message-rpc/uint8-array-message-buffer';
23
+ import { BinaryMessagePipe } from './binary-message-pipe';
24
+
25
+ /**
26
+ * A {@link Channel} to send messages between two processes using a dedicated pipe/fd for binary messages.
27
+ * This fd is opened as 5th channel in addition to the default stdios (stdin,stdout,stderr, ipc). This means the default channels
28
+ * are not blocked and can be used by the respective process for additional custom message handling.
29
+ */
30
+ export class IPCChannel implements Channel {
31
+
32
+ protected readonly onCloseEmitter: Emitter<ChannelCloseEvent> = new Emitter();
33
+ get onClose(): Event<ChannelCloseEvent> {
34
+ return this.onCloseEmitter.event;
35
+ }
36
+
37
+ protected readonly onMessageEmitter: Emitter<MessageProvider> = new Emitter();
38
+ get onMessage(): Event<MessageProvider> {
39
+ return this.onMessageEmitter.event;
40
+ }
41
+
42
+ protected readonly onErrorEmitter: Emitter<unknown> = new Emitter();
43
+ get onError(): Event<unknown> {
44
+ return this.onErrorEmitter.event;
45
+ }
46
+
47
+ protected messagePipe: BinaryMessagePipe;
48
+ protected toDispose = new DisposableCollection();
49
+
50
+ protected ipcErrorListener: (error: Error) => void = error => this.onErrorEmitter.fire(error);
51
+
52
+ constructor(childProcess?: cp.ChildProcess) {
53
+ if (childProcess) {
54
+ this.setupChildProcess(childProcess);
55
+ } else {
56
+ this.setupProcess();
57
+ }
58
+ this.messagePipe.onMessage(message => {
59
+ this.onMessageEmitter.fire(() => new Uint8ArrayReadBuffer(message));
60
+ });
61
+ this.toDispose.pushAll([this.onCloseEmitter, this.onMessageEmitter, this.onErrorEmitter]);
62
+ }
63
+
64
+ protected setupChildProcess(childProcess: cp.ChildProcess): void {
65
+ childProcess.once('exit', code => this.onCloseEmitter.fire({ reason: 'Child process has been terminated', code: code ?? undefined }));
66
+ this.messagePipe = new BinaryMessagePipe(childProcess.stdio[4] as Duplex);
67
+ childProcess.on('error', this.ipcErrorListener);
68
+ this.toDispose.push(Disposable.create(() => {
69
+ childProcess.removeListener('error', this.ipcErrorListener);
70
+ this.messagePipe.dispose();
71
+ }));
72
+ }
73
+
74
+ protected setupProcess(): void {
75
+ process.once('beforeExit', code => this.onCloseEmitter.fire({ reason: 'Process is about to be terminated', code }));
76
+ this.messagePipe = new BinaryMessagePipe(new Socket({ fd: 4 }));
77
+ process.on('uncaughtException', this.ipcErrorListener);
78
+ this.toDispose.push(Disposable.create(() => {
79
+ (process as NodeJS.EventEmitter).removeListener('uncaughtException', this.ipcErrorListener);
80
+ this.messagePipe.dispose();
81
+ }));
82
+ }
83
+
84
+ getWriteBuffer(): WriteBuffer {
85
+ const result = new Uint8ArrayWriteBuffer();
86
+ result.onCommit(buffer => {
87
+ this.messagePipe.send(buffer);
88
+ });
89
+
90
+ return result;
91
+ }
92
+
93
+ close(): void {
94
+ this.toDispose.dispose();
95
+ }
96
+
97
+ }
@@ -15,10 +15,11 @@
15
15
  // *****************************************************************************
16
16
 
17
17
  import * as cp from 'child_process';
18
+ import { inject, injectable } from 'inversify';
18
19
  import * as path from 'path';
19
- import { injectable, inject } from 'inversify';
20
- import { Trace, Tracer, IPCMessageReader, IPCMessageWriter, createMessageConnection, MessageConnection, Message } from 'vscode-ws-jsonrpc';
21
- import { ILogger, ConnectionErrorHandler, DisposableCollection, Disposable } from '../../common';
20
+ import { createInterface } from 'readline';
21
+ import { Channel, ConnectionErrorHandler, Disposable, DisposableCollection, ILogger } from '../../common';
22
+ import { IPCChannel } from './ipc-channel';
22
23
  import { createIpcEnv } from './ipc-protocol';
23
24
 
24
25
  export interface ResolvedIPCConnectionOptions {
@@ -40,7 +41,7 @@ export class IPCConnectionProvider {
40
41
  @inject(ILogger)
41
42
  protected readonly logger: ILogger;
42
43
 
43
- listen(options: IPCConnectionOptions, acceptor: (connection: MessageConnection) => void): Disposable {
44
+ listen(options: IPCConnectionOptions, acceptor: (connection: Channel) => void): Disposable {
44
45
  return this.doListen({
45
46
  logger: this.logger,
46
47
  args: [],
@@ -48,19 +49,21 @@ export class IPCConnectionProvider {
48
49
  }, acceptor);
49
50
  }
50
51
 
51
- protected doListen(options: ResolvedIPCConnectionOptions, acceptor: (connection: MessageConnection) => void): Disposable {
52
+ protected doListen(options: ResolvedIPCConnectionOptions, acceptor: (connection: Channel) => void): Disposable {
52
53
  const childProcess = this.fork(options);
53
- const connection = this.createConnection(childProcess, options);
54
+ const channel = new IPCChannel(childProcess);
54
55
  const toStop = new DisposableCollection();
55
56
  const toCancelStop = toStop.push(Disposable.create(() => childProcess.kill()));
56
57
  const errorHandler = options.errorHandler;
57
58
  if (errorHandler) {
58
- connection.onError((e: [Error, Message | undefined, number | undefined]) => {
59
- if (errorHandler.shouldStop(e[0], e[1], e[2])) {
59
+ let errorCount = 0;
60
+ channel.onError((err: Error) => {
61
+ errorCount++;
62
+ if (errorHandler.shouldStop(err, errorCount)) {
60
63
  toStop.dispose();
61
64
  }
62
65
  });
63
- connection.onClose(() => {
66
+ channel.onClose(() => {
64
67
  if (toStop.disposed) {
65
68
  return;
66
69
  }
@@ -70,36 +73,15 @@ export class IPCConnectionProvider {
70
73
  }
71
74
  });
72
75
  }
73
- acceptor(connection);
76
+ acceptor(channel);
74
77
  return toStop;
75
78
  }
76
79
 
77
- protected createConnection(childProcess: cp.ChildProcess, options: ResolvedIPCConnectionOptions): MessageConnection {
78
- const reader = new IPCMessageReader(childProcess);
79
- const writer = new IPCMessageWriter(childProcess);
80
- const connection = createMessageConnection(reader, writer, {
81
- error: (message: string) => this.logger.error(`[${options.serverName}: ${childProcess.pid}] ${message}`),
82
- warn: (message: string) => this.logger.warn(`[${options.serverName}: ${childProcess.pid}] ${message}`),
83
- info: (message: string) => this.logger.info(`[${options.serverName}: ${childProcess.pid}] ${message}`),
84
- log: (message: string) => this.logger.info(`[${options.serverName}: ${childProcess.pid}] ${message}`)
85
- });
86
- const tracer: Tracer = {
87
- log: (message: unknown, data?: string) => this.logger.debug(`[${options.serverName}: ${childProcess.pid}] ${message}` + (typeof data === 'string' ? ' ' + data : ''))
88
- };
89
- connection.trace(Trace.Verbose, tracer);
90
- this.logger.isDebug().then(isDebug => {
91
- if (!isDebug) {
92
- connection.trace(Trace.Off, tracer);
93
- }
94
- });
95
- return connection;
96
- }
97
-
98
80
  protected fork(options: ResolvedIPCConnectionOptions): cp.ChildProcess {
99
81
  const forkOptions: cp.ForkOptions = {
100
- silent: true,
101
82
  env: createIpcEnv(options),
102
- execArgv: []
83
+ execArgv: [],
84
+ stdio: ['pipe', 'pipe', 'pipe', 'ipc', 'pipe']
103
85
  };
104
86
  const inspectArgPrefix = `--${options.serverName}-inspect`;
105
87
  const inspectArg = process.argv.find(v => v.startsWith(inspectArgPrefix));
@@ -108,8 +90,9 @@ export class IPCConnectionProvider {
108
90
  }
109
91
 
110
92
  const childProcess = cp.fork(path.join(__dirname, 'ipc-bootstrap'), options.args, forkOptions);
111
- childProcess.stdout!.on('data', data => this.logger.info(`[${options.serverName}: ${childProcess.pid}] ${data.toString().trim()}`));
112
- childProcess.stderr!.on('data', data => this.logger.error(`[${options.serverName}: ${childProcess.pid}] ${data.toString().trim()}`));
93
+
94
+ createInterface(childProcess.stdout!).on('line', line => this.logger.info(`[${options.serverName}: ${childProcess.pid}] ${line}`));
95
+ createInterface(childProcess.stderr!).on('line', line => this.logger.error(`[${options.serverName}: ${childProcess.pid}] ${line}`));
113
96
 
114
97
  this.logger.debug(`[${options.serverName}: ${childProcess.pid}] IPC started`);
115
98
  childProcess.once('exit', () => this.logger.debug(`[${options.serverName}: ${childProcess.pid}] IPC exited`));
@@ -15,14 +15,14 @@
15
15
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
16
16
  // *****************************************************************************
17
17
 
18
- import { MessageConnection } from 'vscode-ws-jsonrpc';
18
+ import { Channel } from '../../common/message-rpc/channel';
19
19
 
20
20
  const THEIA_PARENT_PID = 'THEIA_PARENT_PID';
21
21
  const THEIA_ENTRY_POINT = 'THEIA_ENTRY_POINT';
22
22
 
23
23
  export const ipcEntryPoint: string | undefined = process.env[THEIA_ENTRY_POINT];
24
24
 
25
- export type IPCEntryPoint = (connection: MessageConnection) => void;
25
+ export type IPCEntryPoint = (connection: Channel) => void;
26
26
  export namespace IPCEntryPoint {
27
27
  /**
28
28
  * Throws if `THEIA_ENTRY_POINT` is undefined or empty.
@@ -18,19 +18,15 @@ import * as http from 'http';
18
18
  import * as https from 'https';
19
19
  import { Server, Socket } from 'socket.io';
20
20
  import { injectable, inject, named, postConstruct, interfaces, Container } from 'inversify';
21
- import { MessageConnection } from 'vscode-ws-jsonrpc';
22
- import { createWebSocketConnection } from 'vscode-ws-jsonrpc/lib/socket/connection';
23
- import { IConnection } from 'vscode-ws-jsonrpc/lib/server/connection';
24
- import * as launch from 'vscode-ws-jsonrpc/lib/server/launch';
25
21
  import { ContributionProvider, ConnectionHandler, bindContributionProvider } from '../../common';
26
- import { WebSocketChannel } from '../../common/messaging/web-socket-channel';
22
+ import { IWebSocket, WebSocketChannel } from '../../common/messaging/web-socket-channel';
27
23
  import { BackendApplicationContribution } from '../backend-application';
28
- import { MessagingService, WebSocketChannelConnection } from './messaging-service';
29
- import { ConsoleLogger } from './logger';
24
+ import { MessagingService } from './messaging-service';
30
25
  import { ConnectionContainerModule } from './connection-container-module';
31
26
  import Route = require('route-parser');
32
27
  import { WsRequestValidator } from '../ws-request-validators';
33
28
  import { MessagingListener } from './messaging-listeners';
29
+ import { Channel, ChannelMultiplexer } from '../../common/message-rpc/channel';
34
30
 
35
31
  export const MessagingContainer = Symbol('MessagingContainer');
36
32
 
@@ -53,7 +49,7 @@ export class MessagingContribution implements BackendApplicationContribution, Me
53
49
  protected readonly messagingListener: MessagingListener;
54
50
 
55
51
  protected readonly wsHandlers = new MessagingContribution.ConnectionHandlers<Socket>();
56
- protected readonly channelHandlers = new MessagingContribution.ConnectionHandlers<WebSocketChannel>();
52
+ protected readonly channelHandlers = new MessagingContribution.ConnectionHandlers<Channel>();
57
53
 
58
54
  @postConstruct()
59
55
  protected init(): void {
@@ -63,21 +59,7 @@ export class MessagingContribution implements BackendApplicationContribution, Me
63
59
  }
64
60
  }
65
61
 
66
- listen(spec: string, callback: (params: MessagingService.PathParams, connection: MessageConnection) => void): void {
67
- this.wsChannel(spec, (params, channel) => {
68
- const connection = createWebSocketConnection(channel, new ConsoleLogger());
69
- callback(params, connection);
70
- });
71
- }
72
-
73
- forward(spec: string, callback: (params: MessagingService.PathParams, connection: IConnection) => void): void {
74
- this.wsChannel(spec, (params, channel) => {
75
- const connection = launch.createWebSocketConnection(channel);
76
- callback(params, WebSocketChannelConnection.create(connection, channel));
77
- });
78
- }
79
-
80
- wsChannel(spec: string, callback: (params: MessagingService.PathParams, channel: WebSocketChannel) => void): void {
62
+ wsChannel(spec: string, callback: (params: MessagingService.PathParams, channel: Channel) => void): void {
81
63
  this.channelHandlers.push(spec, (params, channel) => callback(params, channel));
82
64
  }
83
65
 
@@ -125,49 +107,31 @@ export class MessagingContribution implements BackendApplicationContribution, Me
125
107
  }
126
108
 
127
109
  protected handleChannels(socket: Socket): void {
110
+ const socketChannel = new WebSocketChannel(this.toIWebSocket(socket));
111
+ const mulitplexer = new ChannelMultiplexer(socketChannel);
128
112
  const channelHandlers = this.getConnectionChannelHandlers(socket);
129
- const channels = new Map<number, WebSocketChannel>();
130
- socket.on('message', data => {
131
- try {
132
- const message: WebSocketChannel.Message = JSON.parse(data.toString());
133
- if (message.kind === 'open') {
134
- const { id, path } = message;
135
- const channel = this.createChannel(id, socket);
136
- if (channelHandlers.route(path, channel)) {
137
- channel.ready();
138
- console.debug(`Opening channel for service path '${path}'. [ID: ${id}]`);
139
- channels.set(id, channel);
140
- channel.onClose(() => {
141
- console.debug(`Closing channel on service path '${path}'. [ID: ${id}]`);
142
- channels.delete(id);
143
- });
144
- } else {
145
- console.error('Cannot find a service for the path: ' + path);
146
- }
147
- } else {
148
- const { id } = message;
149
- const channel = channels.get(id);
150
- if (channel) {
151
- channel.handleMessage(message);
152
- } else {
153
- console.error('The ws channel does not exist', id);
154
- }
155
- }
156
- } catch (error) {
157
- console.error('Failed to handle message', { error, data });
113
+ mulitplexer.onDidOpenChannel(event => {
114
+ if (channelHandlers.route(event.id, event.channel)) {
115
+ console.debug(`Opening channel for service path '${event.id}'.`);
116
+ event.channel.onClose(() => console.debug(`Closing channel on service path '${event.id}'.`));
158
117
  }
159
118
  });
160
- socket.on('error', err => {
161
- for (const channel of channels.values()) {
162
- channel.fireError(err);
163
- }
164
- });
165
- socket.on('disconnect', reason => {
166
- for (const channel of channels.values()) {
167
- channel.close(undefined, reason);
168
- }
169
- channels.clear();
170
- });
119
+ }
120
+
121
+ protected toIWebSocket(socket: Socket): IWebSocket {
122
+ return {
123
+ close: () => {
124
+ socket.removeAllListeners('disconnect');
125
+ socket.removeAllListeners('error');
126
+ socket.removeAllListeners('message');
127
+ socket.disconnect();
128
+ },
129
+ isConnected: () => socket.connected,
130
+ onClose: cb => socket.on('disconnect', reason => cb(reason)),
131
+ onError: cb => socket.on('error', error => cb(error)),
132
+ onMessage: cb => socket.on('message', data => cb(data)),
133
+ send: message => socket.emit('message', message)
134
+ };
171
135
  }
172
136
 
173
137
  protected createSocketContainer(socket: Socket): Container {
@@ -176,7 +140,7 @@ export class MessagingContribution implements BackendApplicationContribution, Me
176
140
  return connectionContainer;
177
141
  }
178
142
 
179
- protected getConnectionChannelHandlers(socket: Socket): MessagingContribution.ConnectionHandlers<WebSocketChannel> {
143
+ protected getConnectionChannelHandlers(socket: Socket): MessagingContribution.ConnectionHandlers<Channel> {
180
144
  const connectionContainer = this.createSocketContainer(socket);
181
145
  bindContributionProvider(connectionContainer, ConnectionHandler);
182
146
  connectionContainer.load(...this.connectionModules.getContributions());
@@ -184,21 +148,12 @@ export class MessagingContribution implements BackendApplicationContribution, Me
184
148
  const connectionHandlers = connectionContainer.getNamed<ContributionProvider<ConnectionHandler>>(ContributionProvider, ConnectionHandler);
185
149
  for (const connectionHandler of connectionHandlers.getContributions(true)) {
186
150
  connectionChannelHandlers.push(connectionHandler.path, (_, channel) => {
187
- const connection = createWebSocketConnection(channel, new ConsoleLogger());
188
- connectionHandler.onConnection(connection);
151
+ connectionHandler.onConnection(channel);
189
152
  });
190
153
  }
191
154
  return connectionChannelHandlers;
192
155
  }
193
156
 
194
- protected createChannel(id: number, socket: Socket): WebSocketChannel {
195
- return new WebSocketChannel(id, content => {
196
- if (socket.connected) {
197
- socket.send(content);
198
- }
199
- });
200
- }
201
-
202
157
  }
203
158
 
204
159
  export namespace MessagingContribution {