Przerwanie odczytu strumienia wyjścia aplikacji

1

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:

  1. Ustawienie timeout odczytu nie jest możliwe, rzuca wyjątek "Limity czasu nie są obsługiwane dla tego strumienia.".
  2. Co prawda Abort() nie jest zalecany, ale nawet, jak się go zastosuje, to problem nie jest rozwiązany, wątek pracuje.
  3. Dodanie chwili czasu na zamknięcie się wątku (3 sekundy) też nic nie daje.

Testuję w ten sposób:

  1. Wywołuje Open("cmd.exe", "");
  2. Wywołuję kilka razy TempStr = new MemoryStream(); Receive(TempStr); i to, co wejdzie do strumienia TempStr, wypisuję na ekran.
  3. Wywołuje Send(Buf);, gdzie Buf jest tablicą bajtówą zawierającą cztery bajty reprezentujące słowo "dir\n",
  4. Znowu wywołuję kilka razy TempStr = new MemoryStream(); Receive(TempStr);.
  5. Wywołuję if (IsConnected()) { Close(); }.

Wszystkie punkty wykonują się zgodnie z oczekiwaniem, a po wywołaniu ostatniego jest ewidentnie coś nie tak.

  1. Jeżeli nie ma Abort(), to na ekranie wypisuje: ()()()()(1234{}Running, co dowodzi, że stoi na BaseStream.Read, a wątek pracuje.
  2. Jeżeli nie ma Abort(), to na ekranie wypisuje: ()()()()(1234{}AbortRequested, co dowodzi, że stoi na BaseStream.Read, wysłano żądanie przerwania, ale wątek wciąż pracuje.
  3. 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;
                }
            }
        }
    }
}
0

Właśnie zauważyłem w funcji Close(), że jeżeli zamiast App.Close(); będzie App.Kill();, czyli od razu brutalne zabicie, to wtedy program działa, jak chcę, czyli wypada z odczytu strumienia i kończy wątek.

W takim razie, jak tego dokonać poprzez zwykłe zamknięcie aplikacji zamiast jej zabijania?

0

Tak tylko spytam to jest jakaś nowa appka ? czy staroć? Bo jest już Net w wersji 7.0 :) (6 LTS)

0
szok napisał(a):

Tak tylko spytam to jest jakaś nowa appka ? czy staroć? Bo jest już Net w wersji 7.0 :) (6 LTS)

Nowa funkcjonalność w staroci. Swoją drogą mam w planach przerobienie do najnowszego .NET, ale nie wiem, czy to coś zmienia.

0

Nie załapałem: co jest celem nadrzędnym, tzw biznesowym ?
Bo jeśli wykonywanie skryptów Powershella - nie robiłem, ale jestem niemal pewien że jest jakieś API, aby to robić in-process z własnego mini startera, albo jako córka.
Wybrałes jedną z trudniejszych, obsługę sióstr

0

Cel biznesowy: możliwość uruchomienia dowolnego programu, który pobiera informacje ze standardowego strumienia wejścia i wypuszcza informacje do standardowego strumienia wyjścia. Innymi słowy, jeżeli ten obcy program jest napisany w C++, to on przyjmuje dane przez std::cin i oddaje dane przez std::cout. Powershell.exe i cmd.exe to "pierwszy z brzegu" program, który spełnia ten warunek, docelowo może być inny program. Np można uruchomić "ping 8.8.8.8", który nic nie pobiera z wejścia, ale na standardowe wyjście wypuszcza określone informacje i się zamyka. Do testów, równie dobrze mógłbym napisać własny program w C++ o funkcjonalności niewiele większej od hello world, czyli coś wczytuje, coś wypisuje, ale niekoniecznie jest to jednorazowa czynność.

Wspominając o połączeniach sieciowych miałem na myśli rzecz analogiczną do telnet. Czyli robię połączenie TCP/IP i potem tylko program coś wysyła, coś odbiera i w każdej chwili może się rozłączyć. Uruchomienie programu jest prawie tym samym z tą różnicą, że w przypadku sieci jest jeden strumień obsługujący transfer w obie strony, a w przypadku programu jest osobny strumień wejścia i osobny strumień wyjścia. Konkretnie, tak naprawdę są dwa strumienie wyjscia, w tym jeden przeznaczony do komunikatów błędów.

1 użytkowników online, w tym zalogowanych: 0, gości: 1