This is possible, although I'm not quite sure it's going to be faster than plink
(it's a native app after all).
To give it a try, I wrote RebexLink
console application and I was able to clone a repository by running this command (please note that not all plink
commands are supported):
hg clone ssh://localhost:8022/reponame --ssh "RebexLink.exe -l demo -pw password" --config ui.username=Test --noninteractive --encoding cp1252 --noupdate --time reponame`
And this is proof-of-concep RebexLink source code (requires Rebex.Common
and Rebex.Networking
assemblies):
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Rebex;
using Rebex.Net;
namespace Rebex.Samples
{
public class RebexLink
{
// buffer size for data proxy
private const int BufferSize = 32 * 1024;
public static int Main(string[] args)
{
// parse arguments
Dictionary<string, string> arguments;
if (!TryParseArguments(args, out arguments))
{
return 1;
}
// get arguments
string hostname = null;
if (!arguments.TryGetValue("hostname", out hostname))
{
Console.Error.WriteLine("Missing hostname");
return 1;
}
string command = null;
if (!arguments.TryGetValue("command", out command))
{
Console.Error.WriteLine("Missing command");
return 1;
}
string username;
if (!arguments.TryGetValue("l", out username))
{
Console.Error.WriteLine("Missing username");
return 1;
}
string password;
if (!arguments.TryGetValue("pw", out password))
{
Console.Error.WriteLine("Missing password");
return 1;
}
int port = 22;
string rawPort;
if (arguments.TryGetValue("p", out rawPort))
{
if (!int.TryParse(rawPort, out port) || port < 1 || port > ushort.MaxValue)
{
Console.Error.WriteLine("Invalid port");
return 1;
}
}
string logFile;
arguments.TryGetValue("log", out logFile);
// establish SSH connection
var ssh = new SshSession();
// enable compression
ssh.Parameters.Compression = true;
if (logFile != null)
ssh.LogWriter = new FileLogWriter(logFile, LogLevel.Debug);
ssh.Connect(hostname, port);
ssh.Authenticate(username, password);
// execute remote command
SshChannel channel = ssh.OpenSession();
channel.RequestExec(command);
// proxy stdin/stdout/stderr
Stream consoleInput = Console.OpenStandardInput();
Stream consoleOutput = Console.OpenStandardOutput();
Stream consoleError = Console.OpenStandardError();
// TODO: Error handling is missing.
// proxy stderr data
channel.ExtendedDataReceived += (sender, e) =>
{
if (e.TypeCode != 2)
return;
consoleError.Write(e.GetData(), 0, e.Length);
};
// proxy stdin data in a background thread
var sendTask = Task.Run(() =>
{
byte[] buffer = new byte[BufferSize];
while (true)
{
int n = consoleInput.Read(buffer, 0, buffer.Length);
if (n == 0)
{
channel.SendEof();
break;
}
channel.Send(buffer, 0, n);
}
});
// proxy stdout data
{
byte[] buffer = new byte[BufferSize];
while (true)
{
int n = channel.Receive(buffer, 0, buffer.Length);
if (n == 0)
{
consoleOutput.Close();
break;
}
consoleOutput.Write(buffer, 0, n);
consoleOutput.Flush();
}
}
// close the channel
channel.Close();
// retrieve exit status
if (channel.ExitStatus != null)
{
return (int)channel.ExitStatus.ExitCode;
}
return 0;
}
private static bool TryParseArguments(string[] args, out Dictionary<string, string> arguments)
{
string hostname = null;
string command = null;
arguments = null;
var result = new Dictionary<string, string>();
for (int i = 0; i < args.Length; i++)
{
string arg = args[i];
if (arg.StartsWith("-"))
{
i++;
if (i == args.Length)
{
Console.Error.WriteLine("Invalid option: " + arg);
return false;
}
result[arg.Substring(1)] = args[i];
}
else
{
if (hostname != null)
{
if (command != null)
{
Console.Error.WriteLine("Unknown argument: " + arg);
return false;
}
command = arg;
}
else
{
hostname = arg;
}
}
}
if (hostname != null)
{
result["hostname"] = hostname;
}
if (command != null)
{
result["command"] = command;
}
arguments = result;
return true;
}
}
}
The core of this actually resembles the server-side, but it's slightly different because the client-side classes don't provide a Stream-based API (yet).