@microsoft/dev-tunnels-ssh
Version:
SSH library for Dev Tunnels
251 lines • 8.87 kB
JavaScript
"use strict";
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebSocketStream = exports.NodeStream = exports.BaseStream = void 0;
const buffer_1 = require("buffer");
const vscode_jsonrpc_1 = require("vscode-jsonrpc");
const cancellation_1 = require("./util/cancellation");
const errors_1 = require("./errors");
function handleCancellation(reject, cancellation) {
if (cancellation) {
if (cancellation.isCancellationRequested) {
reject(new cancellation_1.CancellationError());
}
else {
cancellation.onCancellationRequested(() => {
reject(new cancellation_1.CancellationError());
});
}
}
}
/**
* Base class for stream adapters.
*/
class BaseStream {
constructor() {
this.incomingData = [];
this.pendingReads = [];
this.error = null;
this.disposed = false;
this.closedEmitter = new vscode_jsonrpc_1.Emitter();
this.closed = this.closedEmitter.event;
}
onData(data) {
while (this.pendingReads.length > 0) {
const read = this.pendingReads.shift();
if (read.count >= data.length) {
// This read operation consumes all of the incoming data.
read.resolve(data);
return;
}
else {
// This read operation consumes part of the incoming data.
const readData = data.slice(0, read.count);
data = data.slice(read.count);
read.resolve(readData);
}
}
this.incomingData.push(data);
}
onEnd() {
while (this.pendingReads.length > 0) {
const read = this.pendingReads.shift();
read.resolve(null);
}
this.incomingData.push(buffer_1.Buffer.alloc(0));
}
onError(error) {
if (!this.error) {
this.error = error;
}
while (this.pendingReads.length > 0) {
const read = this.pendingReads.shift();
read.reject(error);
}
}
async read(count, cancellation) {
if (this.disposed)
throw new errors_1.ObjectDisposedError(this);
if (this.incomingData.length > 0) {
// Consume data that has already arrived.
let data = this.incomingData[0];
if (data.length === 0) {
// Reached the end of the stream.
return null;
}
else if (count >= data.length) {
// Consuming the whole chunk.
this.incomingData.shift();
}
else {
// Consuming part of the chunk.
this.incomingData[0] = data.slice(count);
data = data.slice(0, count);
}
return data;
}
else if (this.error) {
throw this.error;
}
else {
// Wait for more data to arrive.
return await new Promise((resolve, reject) => {
if (cancellation) {
if (cancellation.isCancellationRequested) {
reject(new cancellation_1.CancellationError());
return;
}
cancellation.onCancellationRequested(() => {
// Discard any pending reads that use this cancellation token.
for (let i = 0; i < this.pendingReads.length; i++) {
if (Object.is(cancellation, this.pendingReads[i].cancellation)) {
const read = this.pendingReads.splice(i--, 1)[0];
read.reject(new cancellation_1.CancellationError());
}
}
});
}
this.pendingReads.push({ count, resolve, reject, cancellation });
});
}
}
dispose() {
if (!this.disposed) {
this.disposed = true;
const error = new errors_1.ObjectDisposedError(this);
this.onError(error);
this.fireOnClose(error);
}
}
fireOnClose(error) {
this.closedEmitter.fire({ error });
}
get isDisposed() {
return this.disposed;
}
}
exports.BaseStream = BaseStream;
/**
* Stream adapter for a Node.js Socket, Duplex stream, or Readable/Writable stream pair.
*/
class NodeStream extends BaseStream {
constructor(duplexOrReadStream, writeStream) {
super();
if (!duplexOrReadStream)
throw new TypeError('Duplex or Readable/Writable stream are required.');
this.readStream = duplexOrReadStream;
this.writeStream = writeStream || duplexOrReadStream;
this.readStream.on('data', this.onData.bind(this));
this.readStream.on('end', this.onEnd.bind(this));
this.readStream.on('error', this.onError.bind(this));
this.readStream.on('close', () => {
this.onEnd();
this.fireOnClose();
});
}
async write(data, cancellation) {
if (!data)
throw new TypeError('Data is required.');
if (this.disposed)
throw new errors_1.ObjectDisposedError(this);
return new Promise((resolve, reject) => {
handleCancellation(reject, cancellation);
this.writeStream.write(data, (err) => {
if (!err) {
resolve();
}
else {
reject(err);
}
});
});
}
async close(error, cancellation) {
if (this.disposed)
throw new errors_1.ObjectDisposedError(this);
await new Promise((resolve, reject) => {
handleCancellation(reject, cancellation);
this.writeStream.end(resolve);
});
this.disposed = true;
this.onError(error || new errors_1.ObjectDisposedError(this));
this.closedEmitter.fire({ error });
}
dispose() {
if (!this.disposed) {
const error = new errors_1.ObjectDisposedError(this);
this.readStream.destroy(error);
this.writeStream.destroy(error);
}
super.dispose();
}
}
exports.NodeStream = NodeStream;
/**
* Stream adapter for a browser websocket.
*/
class WebSocketStream extends BaseStream {
constructor(websocket) {
super();
this.websocket = websocket;
if (!websocket)
throw new TypeError('WebSocket is required.');
if (typeof websocket.binaryType === 'string' &&
websocket.binaryType !== 'arraybuffer') {
throw new Error('WebSocket must use arraybuffer binary type.');
}
websocket.onmessage = (e) => {
this.onData(buffer_1.Buffer.from(e.data));
};
websocket.onclose = (e) => {
if (e.wasClean) {
this.onEnd();
}
else {
const error = new Error(e.reason);
error.code = e.code;
this.onError(error);
}
};
}
get protocol() {
return this.websocket.protocol;
}
async write(data, cancellation) {
if (!data)
throw new TypeError('Data is required.');
if (this.disposed)
throw new errors_1.ObjectDisposedError(this);
if ('readyState' in this.websocket) {
if (this.websocket.readyState === 2 /* WebSocketReadyState.Closing */ ||
this.websocket.readyState === 3 /* WebSocketReadyState.Closed */) {
throw new DOMException('WebSocket is already in CLOSING or CLOSED state.', 'InvalidStateError');
}
}
this.websocket.send(data);
}
async close(error, cancellation) {
if (this.disposed)
throw new errors_1.ObjectDisposedError(this);
if (!error) {
this.websocket.close();
}
else {
const code = typeof error.code === 'number' ? error.code : undefined;
this.websocket.close(code, error.message);
}
this.disposed = true;
this.closedEmitter.fire({ error });
this.onError(error || new Error('Stream closed.'));
}
dispose() {
if (!this.disposed) {
this.websocket.close();
}
super.dispose();
}
}
exports.WebSocketStream = WebSocketStream;
//# sourceMappingURL=streams.js.map