sources/Google.Solutions.Terminal/Controls/SshShellClient.cs (75 lines of code) (raw):
//
// Copyright 2024 Google LLC
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//
using Google.Solutions.Common.Util;
using Google.Solutions.Platform.IO;
using Google.Solutions.Ssh;
using Google.Solutions.Ssh.Native;
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace Google.Solutions.Terminal.Controls
{
/// <summary>
/// Client that connects a virtual terminal to an SSH shell channel.
/// </summary>
public partial class SshShellClient : PseudoTerminalClientBase
{
private SshConnection? connection;
private void CloseConnection()
{
//
// Close underlying SSH connection. This will cause
// the worker thread to stop, but that happens
// asynchronously.
//
if (this.connection != null)
{
this.connection?.Dispose();
this.connection = null;
}
}
//---------------------------------------------------------------------
// Overrides.
//---------------------------------------------------------------------
protected override async Task<IPseudoTerminal> ConnectCoreAsync(
PseudoTerminalSize initialSize)
{
Debug.Assert(this.connection == null);
var endpoint = this.ServerEndpoint.ExpectNotNull(nameof(this.ServerEndpoint));
var credential = this.Credential.ExpectNotNull(nameof(this.Credential));
//
// Create a new SSH connection.
//
// NB. This might throw various types of Libssh2Exception,
// which are propagated as ConnectionFailed events.
//
var syncContext = SynchronizationContext.Current;
this.connection = new SshConnection(
endpoint,
credential,
new SynchronizedKeyboardInteractiveHandler(
this.KeyboardInteractiveHandler,
syncContext))
{
ConnectionTimeout = this.ConnectionTimeout,
Banner = this.Banner,
//
// Do not join worker thread as this could block the
// UI thread.
//
JoinWorkerThreadOnDispose = false
};
await this.connection
.ConnectAsync()
.ConfigureAwait(false);
//
// Open a shell channel, which acts as a pty.
//
// NB. The channel delivers event on an arbitrary thread,
// but the VirtualTerminal can deal with that. So we
// don't need to worry about forcing callbacks back
// onto a different synchronization context here.
//
return await this.connection
.OpenShellAsync(
initialSize,
this.TerminalType,
this.Locale)
.ConfigureAwait(false);
}
protected override void OnConnectionClosed(DisconnectReason reason)
{
CloseConnection();
base.OnConnectionClosed(reason);
}
protected override void OnConnectionFailed(Exception e)
{
CloseConnection();
base.OnConnectionFailed(e);
}
protected override bool IsCausedByConnectionTimeout(Exception e)
{
return e.Unwrap() is Libssh2Exception sshEx &&
sshEx.ErrorCode == LIBSSH2_ERROR.SOCKET_TIMEOUT;
}
/// <summary>
/// Get underlying SSH connection.
/// </summary>
/// <remarks>
/// The connection must be in LoggedOn state.
/// </remarks>
protected SshConnection Connection
{
get
{
ExpectState(ConnectionState.LoggedOn);
return this.connection!;
}
}
}
}