Potrzebowałem przetestować własnego klienta telnet w Windows 11. To, co chciałem, udało mi się zrobić, ale jest kilka problemów.

Szukałem serwera telnet do Windows 11, ale nie znalazłem, więc postanowiłem na szybko utworzyć własny, prosty serwer w .NET Core 6.0, który tak naprawdę obsługuje jedną aplikację. Najważniejszą aplikacją był vttest, której binarki na Windows nie znalazłem, ale okazało się, że wchodzi w skład pakietu cygwin64. Aplikacja vttest z cygwin64 działa poprawnie w Windows 11.

Serwer telnet miał być niezwykle posty. Uruchamiam wybraną aplikację i do niej wprowadzam wszystko, co przychodzi z klienta i wysyłam do klienta wszystko, co wychodzi z aplikacji. W praktyce okazało się, że bywają trudności z pobraniem danych ze strumienia wyjścia. Aplikację vttest uruchamiam na jeden z trzech sposobów, ale w każdym przypadku są te same problemy:

  1. Jeżeli jest włączone przekierowanie strumienia błędów, to aplikacja nic nie wyświetla (nic nie wypisuje na strumień wyjścia) i nie da się nic zrobić.
  2. Jeżeli jest wyłączone przekierowanie strumienia błędów (zakomentowane zarówno ustawienie przekierowania, jak i utworzenie wątku z pętlą odczytującą) to vttest uruchamia się, ale nie wyświetla menu, trzeba wybierać opcje po omacku. Demonstracje w tym programie działają poprawnie. Nie pomaga wywołanie ?, które odmalowuje menu.
  3. Pomimo podłączenia metody obsługi zdarzeń, ona nie jest wywoływana w przypadku napływania czegokolwiek na strumień wyjścia lub błędów.
  4. Program Midnight Commander da się zdalnie uruchomić, ale nie działają strzałki ani funkcje. Znam dobrze sekwencje bajtów przesyłane przez klienta telnet w dwóch wersjach (bo spotyka się dwie wersje). Jakie sekwencje bajtów należy wysłac na strumień wejścia, żeby program zareagował zgodnie z oczekiwaniem?

Stwierdziłem już, że problemy dotyczą samej obsługi strumieni aplikacji, nie dotyczą połączenia i komunikacji między serwerem a klientem telnet. Jak temu zaradzić? Co robię nieprawidłowo?

using System.Diagnostics;
using System.Net.Sockets;

internal class Program
{
    // Obiekt procesu nadzorujacy zewnetrzny program
    static Process process;

    // Gniazdo i sluchacz w celu umozliwienia nawiazania polaczenia po sieci
    static TcpListener TcpL;
    static Socket TcpS;

    // Wypisywanie informacji do standardowego wejscia programu
    static bool DebugI = false;

    // Wypisywanie informacji ze standardowego wyjscia programu
    static bool DebugO = false;

    static void LoopStrOut()
    {
        LoopOutput(false);
    }

    static void LoopStrErr()
    {
        LoopOutput(true);
    }

    static void LoopOutput(bool Err)
    {
        while (!process.HasExited)
        {
            byte[] Buf = new byte[10000];
            int BufL = 0;
            if (Err)
            {
                BufL = process.StandardError.BaseStream.Read(Buf, 0, 10000);
            }
            else
            {
                BufL = process.StandardOutput.BaseStream.Read(Buf, 0, 10000);
            }
            if (BufL > 0)
            {
                TcpS.Send(Buf, 0, BufL, SocketFlags.None);
                if (DebugO)
                {
                    for (int i = 0; i < BufL; i++)
                    {
                        if ((Buf[i] >= 33) && (Buf[i] <= 126))
                        {
                            Console.Write("_");
                            Console.Write((char)Buf[i]);
                        }
                        else
                        {
                            Console.Write(Buf[i].ToString("X").PadLeft(2, '0'));
                        }
                    }
                }
            }
        }
    }

    static void LoopInput()
    {
        // Przed przyjeciem pierwszego bajtu z terminala, stan jest standardowy, mozliwe sa cztery stany
        int IStreamState = 0;

        // Petla przekazujaca dane z terminala do standardowego wejscia, komendy protokolu Telnet sa ignorowane
        // Po zakonczeniu polaczenia ze strony terminala nie nastepuje opuszczenie petli
        while (TcpS.Connected)
        {
            byte[] Buf = new byte[1024];
            int BufL = TcpS.Receive(Buf);
            for (int i = 0; i < BufL; i++)
            {
                if (DebugI)
                {
                    if ((Buf[i] >= 33) && (Buf[i] <= 126))
                    {
                        Console.Write((char)Buf[i]);
                        Console.Write(" ");
                    }
                    else
                    {
                        Console.Write("_ ");
                    }
                    Console.Write(Buf[i].ToString("X").PadLeft(2, '0'));
                    Console.Write("   " + IStreamState.ToString());
                }

                switch (IStreamState)
                {
                    case 0: // Stan standardowy
                        {
                            if (Buf[i] == 255)
                            {
                                IStreamState = 1;
                            }
                            else
                            {
                                switch (Buf[i])
                                {
                                    default:
                                        process.StandardInput.Write((char)Buf[i]);
                                        break;
                                    case 0: // Ignorowanie bajtu 0
                                    case 10: // Ignorowanie bajtu 10 celem unikniecia efektu dwukrotnego nacisniecia Enter
                                        break;
                                    case 13: // Przychodzacy bajt 13 musi byc wprowadzony do programu jako bajt 10
                                        Buf[i] = 10;
                                        process.StandardInput.Write((char)Buf[i]);
                                        break;
                                }
                            }
                        }
                        break;
                    case 1: // Po przyjeciu znaku nastepujacego po 0xFF
                        {
                            switch (Buf[i])
                            {
                                case 0xFB: // Komenda trzy-znakowa WILL
                                case 0xFC: // Komenda trzy-znakowa WON'T
                                case 0xFD: // Komenda trzy-znakowa DO
                                case 0xFE: // Komenda trzy-znakowa DON'T
                                    IStreamState = 2;
                                    break;
                                case 0xFA: // Komenda dwu-znakowa SUB BEGIN
                                    IStreamState = 3;
                                    break;
                                case 0xF0: // Komenda dwu-znakowa SUB END
                                case 0xFF: // Dwa nastepujace po sobie znaki 0xFF to dwuznakowa komenda
                                    IStreamState = 0;
                                    break;
                            }
                        }
                        break;
                    case 2: // Po przyjeciu trzeciego znaku bedacego elementem trzy-znakowej komendy
                        {
                            IStreamState = 0;
                        }
                        break;
                    case 3: // Stan po przyjeciu komendy SUB BEGIN i oczekiwanie na przyjecie komendy SUB END
                        {
                            if (Buf[i] == 255)
                            {
                                IStreamState = 1;
                            }
                        }
                        break;
                }

                if (DebugI)
                {
                    Console.Write(" -> " + IStreamState.ToString());
                    Console.WriteLine();
                }
            }
        }
    }


    // Wyslanie trzy-znakowej komendy do klienta z inicjatywy serwera
    static void SendCmd(int N1, int N2, int N3)
    {
        byte[] Buf = new byte[3];
        if (N1 >= 0) { Buf[0] = (byte)N1; }
        if (N2 >= 0) { Buf[1] = (byte)N2; }
        if (N3 >= 0) { Buf[2] = (byte)N3; }
        TcpS.Send(Buf, 0, 3, SocketFlags.None);
    }

    private static void Main(string[] args)
    {
        // Tworzenie listy programow
        List<string> ProgName = new List<string>();
        List<string> ProgParam = new List<string>();
        ProgName.Add("C:\\cygwin64\\bin\\vttest.exe");
        ProgParam.Add("24x80.80");
        ProgName.Add("cmd.exe");
        ProgParam.Add("/C C:\\cygwin64\\bin\\vttest.exe 24x80.80");
        ProgName.Add("powershell.exe");
        ProgParam.Add("/C C:\\cygwin64\\bin\\vttest.exe 24x80.80");
        ProgName.Add("cmd.exe");
        ProgParam.Add("");
        ProgName.Add("powershell.exe");
        ProgParam.Add("");
        ProgName.Add("C:\\cygwin64\\bin\\mc.exe");
        ProgParam.Add("");

        // Wybieranie programu
        int Sel = 0;
        for (int i = 0; i < ProgName.Count; i++)
        {
            Console.Write(i);
            Console.Write(". ");
            Console.Write(ProgName[i]);
            Console.Write(" ");
            Console.Write(ProgParam[i]);
            Console.WriteLine();
        }
        Sel = int.Parse(Console.ReadLine());

        // Oczekiwanie na polaczenie na porcie 333
        TcpL = new TcpListener(333);
        TcpL.Start();
        TcpS = TcpL.AcceptSocket();
        TcpL.Stop();

        Console.WriteLine("Connected");

        // Wymuszenie wylaczenia lokalnego echa na kliencie (potrzebne w przypadku niektorych klientow)
        SendCmd(0xFF, 0xFB, 0x01);

        // Tworzenie obiektu typu Process
        process = new Process();

        // Nazwa pliku i parametry wywolawcze programu.
        process.StartInfo.FileName = ProgName[Sel];
        process.StartInfo.Arguments = ProgParam[Sel];

        // Przekierowanie standardowego wejscia i wyjscia
        process.StartInfo.RedirectStandardInput = true;
        process.StartInfo.RedirectStandardOutput = true;
        process.StartInfo.RedirectStandardError = true;

        // Potrzebne w celu przekierowania wejscia i wyjscia
        process.StartInfo.UseShellExecute = false;

        //process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
        //process.StartInfo.CreateNoWindow = true;

        // Podpiecie reakcji na pojawienie sie danych na wyjsciu programu
        process.OutputDataReceived += Process_OutputDataReceived;
        process.ErrorDataReceived += Process_OutputDataReceived;
        process.Start();

        Console.WriteLine("Process running");

        // Uruchamianie petli wprowadzania informacji do wejscia w osobnym watku
        Thread Thr1 = new Thread(LoopInput);
        Thr1.Start();

        // Uruchamianie petli wyciagania informacji z wyjscia informacji w osobnym watku
        Thread Thr2 = new Thread(LoopStrOut);
        Thr2.Start();

        // Uruchamianie petli wyciagania informacji z wyjscia bledow w osobnym watku
        Thread Thr3 = new Thread(LoopStrErr);
        Thr3.Start();

        // Petla przyjmujaca polecenia konfiguracyjne
        bool Work = true;
        while (Work)
        {
            Console.Write("> ");
            string Cmd = Console.ReadLine();
            switch (Cmd.ToUpperInvariant())
            {
                case "EXIT":
                    Work = false;
                    break;
            }
        }

        Console.WriteLine("Disconnecting");

        // Zamkniecie polaczenia sieciowego
        if (TcpS != null)
        {
            if (TcpS.Connected)
            {
                TcpS.Disconnect(false);
                TcpS.Close();
            }
        }

        Console.WriteLine("Killing process");

        // Unicestwienie programu
        if (process != null)
        {
            if (!process.HasExited)
            {
                process.Kill();
            }
        }

        Console.WriteLine("End");

        return;
    }

    // Zdarzenie wywolywane w przypadku wykrycia danych na standardowym wyjsciu,
    // mimo podpiecia do obiektu typu Process nie jest ono wyzwalane
    private static void Process_OutputDataReceived(object sender, DataReceivedEventArgs e)
    {
        Console.Write("*");
    }
}