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:
- 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ć.
- 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. - 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.
- 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("*");
}
}