SSH Client for mercurial

0 votes
asked Mar 14 by vincentp (240 points)
edited Mar 14 by Lukas Pokorny

Is it possible to create a plink like application using rebex. We are using plink.exe (part of putty) as the ssh client with mercurial, eg :

hg.exe clone ssh://localhost:9010// --ssh "\"plink.exe\" -ssh -2 -C -x -a -l abc -pw password -batch" --config ui.username=Test --noninteractive --encoding cp1252 --noupdate --time reponame

I'm love to be able to replicate this functionality (not all of plink, just enough to do what I need) with rebex. I'm hoping it might be faster than plink, and I can avoid plink occasionally popping up dialogs when something goes wrong!

I played with the RExec sample but I guess I need something like what you did for the server side?

Applies to: SSH Pack

1 Answer

0 votes
answered Mar 14 by Lukas Pokorny (82,430 points)

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).

...