Windows 11, .NET Framework 4.8.
Wymyśliłem sobie możliwość podłączenia zewnętrznej aplikacji na podobnej zasadzie, jak podłączenie do serwera sieciowego. Wszystko działa poprawnie za wyjątkiem zamknięcia połączenia.
Próbowałem z najprostszym przypadkiem dostępnym w WIn11, czyli program "cmd.exe" i "powershell.exe". Oba programy współpracują poprawnie, ale z zamknięciem jest ten problem.
Znalazłem analogiczny sposób na strumień reprezentujący połączenie sieciowe:
https://stackoverflow.com/questions/9644588/thread-abort-stucks-at-networkstream-read
https://social.msdn.microsoft.com/Forums/vstudio/en-US/1ab61b3a-823c-4785-b130-d5d253f11d4d/stopping-a-thread-that-is-blocking-on-streamread?forum=csharpgeneral
Zrobiłem dokładnie tak samo (bo co to za różnica, czy to jest połączenie sieciowe, czy I/O obcej aplikacji, zasada działania strumienia powinna być taka sama), ale:
- Ustawienie timeout odczytu nie jest możliwe, rzuca wyjątek "Limity czasu nie są obsługiwane dla tego strumienia.".
- Co prawda
Abort()
nie jest zalecany, ale nawet, jak się go zastosuje, to problem nie jest rozwiązany, wątek pracuje. - Dodanie chwili czasu na zamknięcie się wątku (3 sekundy) też nic nie daje.
Testuję w ten sposób:
- Wywołuje
Open("cmd.exe", "");
- Wywołuję kilka razy
TempStr = new MemoryStream(); Receive(TempStr);
i to, co wejdzie do strumieniaTempStr
, wypisuję na ekran. - Wywołuje
Send(Buf);
, gdzie Buf jest tablicą bajtówą zawierającą cztery bajty reprezentujące słowo"dir\n"
, - Znowu wywołuję kilka razy
TempStr = new MemoryStream(); Receive(TempStr);
. - Wywołuję
if (IsConnected()) { Close(); }
.
Wszystkie punkty wykonują się zgodnie z oczekiwaniem, a po wywołaniu ostatniego jest ewidentnie coś nie tak.
- Jeżeli nie ma
Abort()
, to na ekranie wypisuje:()()()()(1234{}Running
, co dowodzi, że stoi naBaseStream.Read
, a wątek pracuje. - Jeżeli nie ma
Abort()
, to na ekranie wypisuje:()()()()(1234{}AbortRequested
, co dowodzi, że stoi naBaseStream.Read
, wysłano żądanie przerwania, ale wątek wciąż pracuje. - Jeżeli wyślę
exit\n
w celu spowodowania, że aplikacja zewnętrzna sama się zamknie, to wątek sam kończy pracę, zostanie wypisane()()()()#
, co dowodzi przerwanie pętli wątku.
Zarówno z "cmd.exe", jak i "powershell.exe".jest tak samo. Gdzie jest błąd i jak to poprawić, bez zmiany znaczenia metod publicznych z perspektywy poza klasą?
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading;
namespace TextPaint
{
public class UniConnApp : UniConn
{
// Obiekt procesu aplikacji
Process App = null;
// Otwieranie połączenia
public override void Open(string AppName, string Params)
{
App = new Process();
App.StartInfo.FileName = AppName;
App.StartInfo.Arguments = Params;
App.StartInfo.RedirectStandardInput = true;
App.StartInfo.RedirectStandardOutput = true;
App.StartInfo.RedirectStandardError = true;
App.StartInfo.UseShellExecute = false;
if (App.Start())
{
// Rozpoczęcie odczytu strumienia wyjścia
Thr1 = new Thread(ReadO);
Thr1.Start();
}
else
{
App = null;
}
}
// Wątek do obsługi odczytu strumienia wyjścia
Thread Thr1 = null;
// Sprawdzenie, czy w tej chwili jest połączenie
public override bool IsConnected()
{
if (App == null)
{
return false;
}
if (App.HasExited)
{
return false;
}
return true;
}
// Zamknięcie połączenia, z tym jest problem
public override void Close()
{
Console.Write("1");
App.StandardOutput.BaseStream.Close();
Console.Write("2");
App.StandardOutput.Close();
Console.Write("3");
//Thr1.Abort();
Console.Write("4");
Thread.Sleep(3000);
try
{
Console.Write("{");
App.Close();
Console.Write("}");
}
catch
{
try
{
Console.Write("<");
App.Kill();
Console.Write(">");
}
catch
{
}
}
App = null;
Console.Write(Thr1.ThreadState);
}
// Wysłanie ciągu bajtowego (rodzaj kodowania nie ma znaczenia, bo są to znaki z klawiatury)
public override void Send(byte[] Raw)
{
App.StandardInput.Write(Encoding.UTF8.GetString(Raw));
}
// Bufory do odczytu strumieni
byte[] StreamBufO = new byte[1000000];
// Odebranie danych, które napłyneły z aplikacji i nie zostały jeszcze odebrane
// Ta funkcja nie może czekać, aż napłyną nowe dane, może nic nie odebrać
public override void Receive(MemoryStream ms)
{
while (StreamText.Count > 0)
{
ms.WriteByte(StreamText.Dequeue());
}
}
// Kolejka do gromadzenia napłyniętych i nieodebranych danych
Queue<byte> StreamText = new Queue<byte>();
// Odczyt strumienia wyjścia uruchamiany w osobnym wątku
private void ReadO()
{
while (true)
{
try
{
//App.StandardOutput.BaseStream.ReadTimeout = 1000;
Console.Write("(");
int BufL = App.StandardOutput.BaseStream.Read(StreamBufO, 0, StreamBufO.Length);
Console.Write(")");
for (int i = 0; i < BufL; i++)
{
StreamText.Enqueue(StreamBufO[i]);
}
if ((!IsConnected()) && (BufL == 0))
{
Console.Write("#");
break;
}
}
catch (Exception E)
{
Console.Write("@" + E.Message);
break;
}
}
}
}
}