"use strict";
//
//  Copyright (c) Microsoft Corporation. All rights reserved.
//
Object.defineProperty(exports, "__esModule", { value: true });
exports.SshChannel = void 0;
const vscode_jsonrpc_1 = require("vscode-jsonrpc");
const serviceActivation_1 = require("./services/serviceActivation");
const connectionMessages_1 = require("./messages/connectionMessages");
const transportMessages_1 = require("./messages/transportMessages");
const channelMetrics_1 = require("./metrics/channelMetrics");
const promiseCompletionSource_1 = require("./util/promiseCompletionSource");
const errors_1 = require("./errors");
const sshRequestEventArgs_1 = require("./events/sshRequestEventArgs");
const sshChannelClosedEventArgs_1 = require("./events/sshChannelClosedEventArgs");
const cancellation_1 = require("./util/cancellation");
const semaphore_1 = require("./util/semaphore");
const trace_1 = require("./trace");
const pipeExtensions_1 = require("./pipeExtensions");
const queue_1 = require("./util/queue");
/**
 * Represents a channel on an SSH session. A session may include multiple channels, which
 * are multiplexed over the connection. Each channel within a session has a unique integer ID.
 */
class SshChannel {
    /* @internal */
    constructor(connectionService, channelType, channelId, remoteChannelId, remoteMaxWindowSize, remoteMaxPacketSize, openMessage, openConfirmationMessage) {
        this.connectionService = connectionService;
        this.channelType = channelType;
        this.channelId = channelId;
        this.remoteChannelId = remoteChannelId;
        this.openMessage = openMessage;
        this.openConfirmationMessage = openConfirmationMessage;
        this.remoteClosed = false;
        this.localClosed = false;
        this.sentEof = false;
        this.disposed = false;
        this.openSendingWindowCompletionSource = null;
        this.requestCompletionSources = new queue_1.Queue();
        this.sendSemaphore = new semaphore_1.Semaphore(0);
        /**
         * Gets an object that reports measurements about the channel.
         */
        this.metrics = new channelMetrics_1.ChannelMetrics();
        this.dataReceivedEmitter = new vscode_jsonrpc_1.Emitter();
        this.extendedDataReceivedEmitter = new vscode_jsonrpc_1.Emitter();
        /**
         * Event raised when a data message is received on the channel.
         *
         * Users of a channel MUST add a `onDataReceived` event handler immediately after a
         * channel is opened/accepted, or else all session communication will be blocked.
         * (The `SshStream` class does this automatically.)
         *
         * The event handler must call `adjustWindow` when the data has been consumed,
         * to notify the remote side that it may send more data.
         */
        this.onDataReceived = this.dataReceivedEmitter.event;
        this.onExtendedDataReceived = this.extendedDataReceivedEmitter.event;
        this.eofEmitter = new vscode_jsonrpc_1.Emitter();
        /**
         * Event raised when an EOF message is received on the channel.
         */
        this.onEof = this.eofEmitter.event;
        this.closedEmitter = new vscode_jsonrpc_1.Emitter();
        this.onClosed = this.closedEmitter.event;
        this.requestEmitter = new vscode_jsonrpc_1.Emitter();
        this.onRequest = this.requestEmitter.event;
        /**
         * Gets or sets a value indicating whether `maxWindowSize is locked, so that it cannot be
         * changed after the channel is opened.
         */
        /* @internal */
        this.isMaxWindowSizeLocked = false;
        this.remoteWindowSize = remoteMaxWindowSize;
        this.maxWindowSizeValue = SshChannel.defaultMaxWindowSize;
        this.windowSize = this.maxWindowSizeValue;
        this.maxPacketSize = Math.min(remoteMaxPacketSize, SshChannel.defaultMaxPacketSize);
    }
    get session() {
        return this.connectionService.session;
    }
    get isClosed() {
        return this.localClosed || this.remoteClosed;
    }
    /**
     * Gets the maximum window size for received data. The other side will not send more
     * data than the window size until it receives an acknowledgement that some of the data was
     * received and processed by this side.
     */
    get maxWindowSize() {
        return this.maxWindowSizeValue;
    }
    /**
     * Sets the maximum window size for received data. The other side will not send more
     * data than the window size until it receives an acknowledgement that some of the data was
     * received and processed by this side.
     *
     * The default value is `defaultMaxWindowSize`. The value may be configured for a channel
     * opened by this side by setting `ChannelOpenMessage.maxWindowSize` in the message object
     * passed to `SshSession.openChannel()`, or for a channel opened by the other side by
     * assigning to this property while handling the `SshSession.onChannelOpening` event.
     * Changing the maximum window size at any other time is not valid because the other
     * side would not be aware of the change.
     */
    set maxWindowSize(value) {
        if (this.isMaxWindowSizeLocked) {
            throw new Error('Cannot change the max window size after opening the channel.');
        }
        if (value < this.maxPacketSize) {
            throw new Error('Maximum window size cannot be less than maximum packet size.');
        }
        this.maxWindowSizeValue = value;
    }
    /**
     * Sends a channel request and waits for a response.
     *
     * Note if `wantReply` is `false`, this method returns `true` immediately after sending the
     * request, without waiting for a reply.
     *
     * @returns The authorization status of the response; if false, the other side denied the
     * request.
     * @throws `ObjectDisposedError` if the channel was closed before sending the request.
     * @throws `SshChannelError` if the channel was closed while waiting for a reply to the request.
     */
    async request(request, cancellation) {
        if (!request)
            throw new TypeError('Request is required.');
        if (this.disposed)
            throw new errors_1.ObjectDisposedError(this);
        request.recipientChannel = this.remoteChannelId;
        if (!request.wantReply) {
            // If a reply is not requested, there's no need to set up a completion source.
            await this.session.sendMessage(request, cancellation);
            return true;
        }
        const requestCompletionSource = new promiseCompletionSource_1.PromiseCompletionSource();
        if (cancellation) {
            if (cancellation.isCancellationRequested)
                throw new cancellation_1.CancellationError();
            cancellation.onCancellationRequested(() => {
                requestCompletionSource.reject(new cancellation_1.CancellationError());
            });
        }
        this.requestCompletionSources.enqueue(requestCompletionSource);
        await this.session.sendMessage(request, cancellation);
        return await requestCompletionSource.promise;
    }
    async send(data, cancellation) {
        return this.sendCommon(data, undefined, cancellation);
    }
    async sendExtendedData(dataTypeCode, data, cancellation) {
        return this.sendCommon(data, dataTypeCode, cancellation);
    }
    async sendCommon(data, extendedDataType, cancellation) {
        if (this.disposed)
            throw new errors_1.ObjectDisposedError(this);
        if (data.length === 0) {
            await this.sendEof();
            return;
        }
        else if (this.sentEof) {
            throw new Error('Cannot send more data after EOF.');
        }
        // Prevent out-of-order message chunks even if the caller does not await.
        // Also don't send until the channel is fully opened.
        await this.sendSemaphore.wait(cancellation);
        try {
            let offset = 0;
            let count = data.length;
            while (count > 0) {
                let packetSize = Math.min(Math.min(this.remoteWindowSize, this.maxPacketSize), count);
                while (packetSize === 0) {
                    if (!this.openSendingWindowCompletionSource) {
                        this.openSendingWindowCompletionSource = new promiseCompletionSource_1.PromiseCompletionSource();
                    }
                    this.session.trace(trace_1.TraceLevel.Warning, trace_1.SshTraceEventIds.channelWaitForWindowAdjust, `${this} send window is full. Waiting for window adjustment before sending.`);
                    await (0, cancellation_1.withCancellation)(this.openSendingWindowCompletionSource.promise, cancellation);
                    this.openSendingWindowCompletionSource = null;
                    packetSize = Math.min(Math.min(this.remoteWindowSize, this.maxPacketSize), count);
                }
                let msg;
                if (extendedDataType !== undefined) {
                    msg = new connectionMessages_1.ChannelExtendedDataMessage();
                    msg.dataTypeCode = extendedDataType;
                }
                else {
                    msg = new connectionMessages_1.ChannelDataMessage();
                }
                msg.recipientChannel = this.remoteChannelId;
                // Unfortunately the data must be copied to a new buffer at this point
                // to ensure it is still available to be re-sent later in case of disconnect.
                msg.data = Buffer.from(data.slice(offset, offset + packetSize));
                await this.session.sendMessage(msg, cancellation);
                this.remoteWindowSize -= packetSize;
                count -= packetSize;
                offset += packetSize;
                this.metrics.addBytesSent(packetSize);
            }
        }
        finally {
            this.sendSemaphore.tryRelease();
        }
    }
    /* @internal */
    enableSending() {
        this.sendSemaphore.tryRelease();
    }
    async sendEof(cancellation) {
        if (this.sentEof) {
            return;
        }
        await this.sendSemaphore.wait(cancellation);
        try {
            this.sentEof = true;
            const msg = new connectionMessages_1.ChannelEofMessage();
            msg.recipientChannel = this.remoteChannelId;
            await this.session.sendMessage(msg, cancellation);
        }
        finally {
            this.sendSemaphore.tryRelease();
        }
    }
    /* @internal */
    async handleRequest(request, cancellation) {
        if (!request.requestType) {
            throw new errors_1.SshConnectionError('Channel request type not specified.', transportMessages_1.SshDisconnectReason.protocolError);
        }
        if (request.requestType === connectionMessages_1.ChannelRequestType.exitStatus) {
            const signal = new connectionMessages_1.ChannelSignalMessage();
            request.convertTo(signal);
            this.exitStatus = signal.exitStatus;
            return true;
        }
        else if (request.requestType === connectionMessages_1.ChannelRequestType.exitSignal) {
            const signal = new connectionMessages_1.ChannelSignalMessage();
            request.convertTo(signal);
            this.exitSignal = signal.exitSignal;
            this.exitErrorMessage = signal.errorMessage;
            return true;
        }
        else if (request.requestType === connectionMessages_1.ChannelRequestType.signal) {
            const signal = new connectionMessages_1.ChannelSignalMessage();
            request.convertTo(signal);
        }
        const args = new sshRequestEventArgs_1.SshRequestEventArgs(request.requestType, request, this.session.principal, cancellation);
        const serviceType = (0, serviceActivation_1.findService)(this.session.config.services, (a) => (!a.channelType || a.channelType === this.channelType) &&
            a.channelRequest === request.requestType);
        await this.sendSemaphore.wait(cancellation);
        try {
            let response = null;
            if (serviceType) {
                // A service was configured for activation via this session request type.
                const service = this.session.activateService(serviceType);
                // `onChannelRequest` should really be 'protected internal'.
                await service.onChannelRequest(this, args, cancellation);
            }
            else {
                this.requestEmitter.fire(args);
            }
            // TODO: do not block requests in TS (similar to CS)
            // see https://dev.azure.com/devdiv/DevDiv/_git/SSH/commit/0b84a48811e2f015107c73bf4584b6c3b676a6de
            if (args.responsePromise) {
                response = await args.responsePromise;
                args.isAuthorized = response instanceof connectionMessages_1.ChannelSuccessMessage;
            }
            if (request.wantReply) {
                if (args.isAuthorized) {
                    response = response !== null && response !== void 0 ? response : new connectionMessages_1.ChannelSuccessMessage();
                    response.recipientChannel = this.remoteChannelId;
                }
                else {
                    response = response !== null && response !== void 0 ? response : new connectionMessages_1.ChannelFailureMessage();
                    response.recipientChannel = this.remoteChannelId;
                }
                await this.session.sendMessage(response, cancellation);
            }
        }
        finally {
            this.sendSemaphore.tryRelease();
        }
        return args.isAuthorized || false;
    }
    /* @internal */
    handleResponse(result) {
        const requestCompletionSource = this.requestCompletionSources.dequeue();
        if (requestCompletionSource) {
            requestCompletionSource.resolve(result);
        }
    }
    /* @internal */
    handleDataReceived(data) {
        this.metrics.addBytesReceived(data.length);
        // DataReceived handler is to adjust the window when it's done with the data.
        this.dataReceivedEmitter.fire(data);
    }
    handleExtendedDataReceived(data) {
        this.metrics.addBytesReceived(data.data.length);
        this.extendedDataReceivedEmitter.fire(data);
    }
    /**
     * Adjusts the local receiving window size by the specified amount, notifying
     * the remote side that it is free to send more data.
     *
     * This method MUST be called either immediately or eventually by the
     * `onDataReceived` event handler as incoming data is processed.
     */
    adjustWindow(messageLength) {
        if (this.disposed)
            return;
        this.windowSize -= messageLength;
        if (this.windowSize <= this.maxWindowSizeValue / 2) {
            const windowAdjustMessage = new connectionMessages_1.ChannelWindowAdjustMessage();
            windowAdjustMessage.recipientChannel = this.remoteChannelId;
            windowAdjustMessage.bytesToAdd = this.maxWindowSizeValue - this.windowSize;
            this.windowSize = this.maxWindowSizeValue;
            this.session.sendMessage(windowAdjustMessage).catch((e) => {
                this.session.trace(trace_1.TraceLevel.Error, trace_1.SshTraceEventIds.channelWindowAdjustFailed, `Error sending window adjust message: ${e.message}`, e);
            });
        }
    }
    /* @internal */
    adjustRemoteWindow(bytesToAdd) {
        this.remoteWindowSize += bytesToAdd;
        if (this.openSendingWindowCompletionSource) {
            this.openSendingWindowCompletionSource.resolve(undefined);
        }
    }
    /* @internal */
    handleEof() {
        this.session.trace(trace_1.TraceLevel.Info, trace_1.SshTraceEventIds.channelEofReceived, `${this} EOF received.`);
        this.eofEmitter.fire();
    }
    close(exitStatusOrSignal, errorMessage, cancellation) {
        if (exitStatusOrSignal instanceof Error) {
            const error = exitStatusOrSignal;
            if (!this.localClosed) {
                this.localClosed = true;
                this.session.trace(trace_1.TraceLevel.Info, trace_1.SshTraceEventIds.channelClosed, `${this} Closed: ${error.message}`);
                this.closedEmitter.fire(new sshChannelClosedEventArgs_1.SshChannelClosedEventArgs(error));
            }
            this.disposeInternal();
            return;
        }
        if (typeof exitStatusOrSignal === 'number') {
            return this.closeWithStatus(exitStatusOrSignal, errorMessage);
        }
        else if (typeof exitStatusOrSignal === 'string') {
            return this.closeWithSignal(exitStatusOrSignal, errorMessage, cancellation);
        }
        else {
            return this.closeDefault(exitStatusOrSignal);
        }
    }
    async closeDefault(cancellation) {
        if (!this.remoteClosed && !this.localClosed) {
            this.remoteClosed = true;
            await this.sendSemaphore.wait(cancellation);
            try {
                const closeMessage = new connectionMessages_1.ChannelCloseMessage();
                closeMessage.recipientChannel = this.remoteChannelId;
                await this.session.sendMessage(closeMessage);
            }
            catch (e) {
                // The session was already closed.
            }
            finally {
                this.sendSemaphore.tryRelease();
            }
        }
        if (!this.localClosed) {
            this.localClosed = true;
            const closedMessage = this.raiseClosedEvent();
        }
        this.disposeInternal();
    }
    async closeWithStatus(exitStatus, cancellation) {
        if (!this.remoteClosed && !this.localClosed) {
            this.exitStatus = exitStatus;
            const signalMessage = new connectionMessages_1.ChannelSignalMessage();
            signalMessage.recipientChannel = this.remoteChannelId;
            signalMessage.exitStatus = exitStatus;
            await this.session.sendMessage(signalMessage);
        }
        await this.closeDefault(cancellation);
    }
    async closeWithSignal(exitSignal, errorMessage, cancellation) {
        if (!this.remoteClosed && !this.localClosed) {
            this.exitSignal = exitSignal;
            this.exitErrorMessage = errorMessage;
            const signalMessage = new connectionMessages_1.ChannelSignalMessage();
            signalMessage.recipientChannel = this.remoteChannelId;
            signalMessage.exitSignal = exitSignal;
            signalMessage.errorMessage = errorMessage;
            await this.session.sendMessage(signalMessage);
        }
        await this.closeDefault(cancellation);
    }
    /* @internal */
    handleClose() {
        if (!this.localClosed) {
            this.localClosed = true;
            const closedMessage = this.raiseClosedEvent(true);
        }
        this.disposeInternal();
    }
    raiseClosedEvent(closedByRemote = false) {
        const metricsMessage = ` (S: ${this.metrics.bytesSent}, R: ${this.metrics.bytesReceived})`;
        const originMessage = closedByRemote ? 'remotely' : 'locally';
        let closedMessage;
        let args;
        if (typeof this.exitStatus !== 'undefined') {
            args = new sshChannelClosedEventArgs_1.SshChannelClosedEventArgs(this.exitStatus);
            closedMessage = `${this} closed ${originMessage}: status=${this.exitStatus}`;
        }
        else if (typeof this.exitSignal !== 'undefined') {
            args = new sshChannelClosedEventArgs_1.SshChannelClosedEventArgs(this.exitSignal, this.exitErrorMessage);
            closedMessage = `${this} closed ${originMessage}: signal=${this.exitSignal} ${this.exitErrorMessage}`;
        }
        else {
            args = new sshChannelClosedEventArgs_1.SshChannelClosedEventArgs();
            closedMessage = `${this} closed ${originMessage}.`;
        }
        this.session.trace(trace_1.TraceLevel.Info, trace_1.SshTraceEventIds.channelClosed, closedMessage + metricsMessage);
        this.closedEmitter.fire(args);
        return closedMessage;
    }
    dispose() {
        if (!this.disposed && !this.localClosed) {
            if (!this.remoteClosed) {
                this.remoteClosed = true;
                const closeMessage = new connectionMessages_1.ChannelCloseMessage();
                closeMessage.recipientChannel = this.remoteChannelId;
                this.session.sendMessage(closeMessage).catch((e) => {
                    // The session was already closed, or some other sending error occurred.
                    // The details have already been traced.
                });
            }
            const message = this.session.isClosed ? `${this.session} closed.` : `${this} disposed.`;
            this.session.trace(trace_1.TraceLevel.Info, trace_1.SshTraceEventIds.channelClosed, message);
            const args = new sshChannelClosedEventArgs_1.SshChannelClosedEventArgs('SIGABRT', message);
            this.localClosed = true;
            this.closedEmitter.fire(args);
        }
        this.disposeInternal();
    }
    disposeInternal() {
        if (this.disposed)
            return;
        this.disposed = true;
        this.cancelPendingRequests();
        this.connectionService.removeChannel(this);
        this.sendSemaphore.dispose();
    }
    /**
     * Pipes one SSH channel into another, relaying all data between them.
     * @param toChannel Channel to which the current channel will be connected via the pipe.
     * @returns A promise that resolves when the channels are closed.
     */
    pipe(toChannel) {
        return pipeExtensions_1.PipeExtensions.pipeChannel(this, toChannel);
    }
    cancelPendingRequests() {
        for (const completion of this.requestCompletionSources) {
            completion.resolve(false);
        }
    }
    toString() {
        return `SshChannel(Type: ${this.channelType}, Id: ${this.channelId}, RemoteId: ${this.remoteChannelId})`;
    }
}
exports.SshChannel = SshChannel;
SshChannel.sessionChannelType = 'session';
/**
 * Default maximum packet size. Channel data payloads larger than the max packet size will
 * be broken into chunks before sending. The actual `maxPacketSize` may be smaller (but
 * never larger) than the default if requested by the other side.
 */
SshChannel.defaultMaxPacketSize = connectionMessages_1.ChannelOpenMessage.defaultMaxPacketSize;
/**
 * Default maximum window size for received data. The other side will not send more data than
 * the window size until it receives an acknowledgement that some of the data was received and
 * processed by this side. A non-default `maxWindowSize` may be configured at the time of
 * opening the channel.
 */
SshChannel.defaultMaxWindowSize = connectionMessages_1.ChannelOpenMessage.defaultMaxWindowSize;
//# sourceMappingURL=https://main.vscode-cdn.net/sourcemaps/f6cfa2ea2403534de03f069bdf160d06451ed282/node_modules/@microsoft/dev-tunnels-ssh/sshChannel.js.map