2016-01-05 59 views
9

Robię małą aplikację ncurses w Rust, która musi komunikować się z procesem podrzędnym. Mam już prototyp napisany w Common Lisp; GIF here ma nadzieję, że pokażę, co chcę zrobić. Próbuję przepisać to, ponieważ CL używa ogromnej ilości pamięci dla tak małego narzędzia.Jak odczytać dane wyjściowe procesu potomnego bez blokowania w Rust?

Nie użyłem Rusta przed (lub innymi niskopoziomowymi językami) i mam problem z ustaleniem sposobu interakcji z pod-procesem.

Co mam obecnie robi to mniej więcej w ten sposób:

  1. Tworzenie procesu:

    let mut program = match Command::new(command) 
        .args(arguments) 
        .stdin(Stdio::piped()) 
        .stdout(Stdio::piped()) 
        .stderr(Stdio::piped()) 
        .spawn() { 
         Ok(child) => child, 
         Err(_) => { 
          println!("Cannot run program '{}'.", command); 
          return; 
         }, 
        }; 
    
  2. przekazać go do nieskończonej (do użytkownika) wyjść pętli, która odczytuje i obsługuje wejście i słucha takich wyników (i zapisuje je na ekranie):

    fn listen_for_output(program: &mut Child, 
            output_viewer: &TextViewer) { 
        match program.stdout { 
         Some(ref mut out) => { 
          let mut buf_string = String::new(); 
          match out.read_to_string(&mut buf_string) { 
           Ok(_) => output_viewer.append_string(buf_string), 
           Err(_) => return, 
          }; 
         }, 
         None => return, 
        }; 
    } 
    

Jednak wywołanie read_to_string blokuje program do momentu zakończenia procesu. Z tego, co widzę, read_to_end i read również wydaje się blokować. Jeśli spróbuję uruchomić coś w rodzaju ls, które od razu się kończy, działa, ale z czymś, co nie kończy się tak jak python lub sbcl, działa ono tylko po ręcznym zabiciu podprocesu.

Edit:

podstawie this answer, zmieniłem kod do korzystania BufReader:

fn listen_for_output(program: &mut Child, 
        output_viewer: &TextViewer) { 
    match program.stdout.as_mut() { 
     Some(out) => { 
      let buf_reader = BufReader::new(out); 
      for line in buf_reader.lines() { 
       match line { 
        Ok(l) => { 
         output_viewer.append_string(l); 
        }, 
        Err(_) => return, 
       }; 
      } 
     }, 
     None => return, 
    } 
} 

Jednak problem nadal pozostaje taka sama. Będzie czytać wszystkie linie, które są dostępne, a następnie blokować. Ponieważ narzędzie ma współpracować z dowolnym programem, nie ma możliwości odgadnięcia, kiedy wyjście się zakończy, przed próbą odczytu. Wydaje się, że nie ma sposobu na ustawienie limitu czasu dla BufReader.

Odpowiedz

11

Domyślnie strumienie są blokujące. Strumienie TCP/IP, strumienie systemu plików, strumienie potoków, wszystkie są blokowane. Kiedy powiesz strumieniowi, że dostaniesz kawałek bajtów, zatrzyma się i poczeka, aż otrzyma daną ilość bajtów lub dopóki coś innego się nie stanie (interrupt, koniec strumienia, błąd).

Systemy operacyjne chcą przywrócić dane do procesu odczytu, więc jeśli chcesz tylko poczekać na następny wiersz i obsłużyć go, jak tylko pojawi się, działa metoda sugerowana przez Shepmaster w Unable to pipe to or from spawned child process more than once. (Teoretycznie nie musi, ponieważ system operacyjny może sprawić, że BufReader czeka na więcej danych w read, ale w praktyce systemy operacyjne preferują wczesne "krótkie odczyty" do czekania).

To proste podejście oparte na protokole BufReader przestaje działać, gdy trzeba obsługiwać wiele strumieni (np. stdout i stderr procesu podrzędnego) lub wielu procesów. Na przykład podejście oparte na BufReader może zakleszczać się, gdy proces potomny czeka na ciebie, aby opróżnić rurę stderr, podczas gdy twój proces jest blokowany, czekając na pusty stdout.

Podobnie, nie można użyć BufReader, gdy nie chcesz, aby twój program czekał na proces potomny przez czas nieokreślony. Być może chcesz wyświetlić pasek postępu lub timer, gdy dziecko nadal pracuje i nie daje żadnych wyników.

Nie można zastosować podejścia opartego na BufReader, jeśli system operacyjny nie jest chętny do przywrócenia danych do procesu (preferuje "pełne odczyty" do "krótkich odczytów"), ponieważ w tym przypadku wydrukowano kilka ostatnich linii proces potomny może znaleźć się w szarej strefie: system operacyjny je ma, ale nie są wystarczająco duże, aby wypełnić bufor BufReader.

BufReader ogranicza się do tego, co pozwala interfejs Read ze strumieniem, nie mniej blokuje niż strumień bazowy. Aby być wydajnym, będzie on wprowadzał fragmenty, mówiąc systemowi operacyjnemu, aby zapełnił jak najwięcej swojego bufora, jaki jest dostępny.

Być może zastanawiacie się, dlaczego czytanie danych w kawałkach jest tutaj tak ważne, dlaczego nie można odczytać bajtu danych po bajcie przez BufReader. Problem polega na tym, że aby odczytać dane ze strumienia, potrzebujemy pomocy systemu operacyjnego. Z drugiej strony, nie jesteśmy systemem operacyjnym, pracujemy od niego odizolowani, aby nie zepsuć go, jeśli coś pójdzie nie tak z naszym procesem. Aby więc zadzwonić do systemu operacyjnego, musi nastąpić przejście do "trybu jądra", który może również spowodować "przełączenie kontekstu". Dlatego wywołanie systemu operacyjnego do czytania każdego bajtu jest drogie. Chcemy jak najmniej połączeń systemowych, więc otrzymujemy dane strumieniowe w partiach.

Aby poczekać na transmisję bez blokowania, potrzebny jest niezablokowany strumień . MIO promises to have the required non-blocking stream support for pipes, najprawdopodobniej z PipeReader, ale nie sprawdziłem go do tej pory.

Nie blokujący charakter strumienia powinien umożliwiać odczytywanie danych w porcjach, niezależnie od tego, czy system operacyjny preferuje "krótkie odczyty", czy nie. Ponieważ nie blokujący strumień nigdy nie blokuje. Jeśli w strumieniu nie ma żadnych danych, po prostu to mówi.

W absencji nie blokującego strumienia będziesz musiał uciekać się do wątków odradzania, aby odczyty blokujące były wykonywane w osobnym wątku, a tym samym nie blokują twojego głównego wątku. Możesz również czytać bajt strumienia bajtowo, aby natychmiast zareagować na separator linii w przypadku, gdy system operacyjny nie preferuje "krótkich odczytów". Oto działający przykład: https://gist.github.com/ArtemGr/db40ae04b431a95f2b78.

+0

Dzięki za pomocne wyjaśnienie. Zajrzę do MIO, a jeśli to nie zadziała, użyję osobnych wątków. – jkiiski