Weźmy na tapetę taki program:
print("Started")
while True:
pass
Jeśli go uruchomię, i podczas jego działania spróbuję go ubić, np przez Ctrl+C, to zobaczę takie coś:
PS C:\Users\Riddle\PycharmProjects\x> python .\filex.py
Started
Traceback (most recent call last):
File "C:\Users\Riddle\PycharmProjects\x\filex.py", line 1, in <module>
while True:
KeyboardInterrupt
Z czego "Started" leci na standard output, a stacktrace leci na standard output.
Chciałbym teraz zrobić to samo programistycznie, czyli:
- Zespawnować proces
- Wczytać string
"Started"
- Wysłać sygnał interrupt (dowolną metodą, to może być signal, jakiś znak na standard input, ubicie procesu, dowolnie)
- Odczytać stacktrace.
Na razie zakodziłem coś takiego:
import signal
import threading
from subprocess import PIPE, Popen
def run_process():
print("Starting process...")
source_code = """
print("Started")
while True:
pass
"""
proc = Popen(["python", "-c", source_code], stdout=PIPE, stdin=PIPE)
print("Process started")
return proc
def send_interrupt(process: Popen) -> None:
print("Sending interrupt...")
process.send_signal(signal.CTRL_BREAK_EVENT)
process.send_signal(signal.CTRL_C_EVENT)
print("Interrupt sent")
def read_output(process: Popen) -> bytes:
print("Reading output...")
output = b""
while True:
line = process.stdout.readline()
if not line:
break
output += line
print(line.decode(), end="")
print("Output read")
return output
process = run_process()
threading.Timer(2.0, send_interrupt, args=[process]).start()
output = read_output(process)
print("Process finished with output:")
print(output.decode())
ale wysłanie signal.CTRL_BREAK_EVENT
oraz signal.CTRL_C_EVENT
nie ubija programu. Wysłanie b"\x03"
(czyli ASCII EXT
) na standard input też nie obija programu (to chyba działa tylko z shella).
import subprocess
from subprocess import PIPE
command = ["python", "-c", """
x = input();
print(x);
print("Finished");
"""]
process = subprocess.Popen(command, stdout=PIPE, stdin=PIPE, stderr=PIPE)
communicate = process.communicate(b"\x03")
print(communicate)
print("Process finished with exit code:", process.returncode)
Ten kod po prostu "łyka" ten \x03
, nie można tego użyć żeby ubić proces.
(b'\x03\r\nFinished\r\n', b'')
Process finished with exit code: 0
Jeśli po prostu rzucę KeyboardInterrupt()
, to udaje mi się go złapać:
import subprocess
from subprocess import PIPE
command = ["python", "-c", """
raise KeyboardInterrupt()
"""]
process = subprocess.Popen(command, stdout=PIPE, stdin=PIPE, stderr=PIPE)
print(process.communicate())
print("Process finished with exit code:", process.returncode)
Process finished with exit code: 3221225786
(b'', b'Traceback (most recent call last):\r\n File "<string>", line 2, in <module>\r\nKeyboardInterrupt\r\n')
Dostaję stacktrace w wyniku, czyli dokładnie to o co mi chodzi - to jest spodziewany efekt, żeby wywołać KeyboardInterrupt
i wczytać stacktrace, tak jak w tym przykładzie. Ale chciałbym wywołać go nie przez wpisanie raise KeyboardInterrupt()
, tylko poprzez wysłanie sygnału, np tak:
import signal
import subprocess
from subprocess import PIPE
command = ["python", "-c", """
print("Started")
while True:
pass
print("Finished")
"""]
process = subprocess.Popen(command, stdout=PIPE, stdin=PIPE, stderr=PIPE)
process.send_signal(signal.CTRL_BREAK_EVENT)
process.send_signal(signal.CTRL_C_EVENT)
print(process.communicate())
print("Process finished with exit code:", process.returncode)
(b'', b'')
Process finished with exit code: 3221225794
Tylko wtedy już nie udaje mi się dostać stacktrace'a. Nie jestem do końca pewien dlaczego, wydaje mi się że może być tak że sygnały trafią za szybko do procesu, i ubijają go zanim w ogóle wstanie.
Więc pomyślałem że zrobię coś takiego: spawn proces, wczytaj jego stdout, niech on teraz blokuje czekając na stdin, wyślij coś na stdin i od razu wyślij sygnał interrupta:
import signal
import subprocess
from subprocess import PIPE
command = ["python", "-c", """
print("Started")
value = input()
print("received: " + value)
while True:
pass
"""]
process = subprocess.Popen(command, stdout=PIPE, stdin=PIPE, stderr=PIPE)
print("read line: " + process.stdout.readline().decode())
process.stdin.write(b"input\n")
process.stdin.flush()
process.send_signal(signal.CTRL_BREAK_EVENT)
process.send_signal(signal.CTRL_C_EVENT)
print(process.communicate())
print("Process finished with exit code:", process.returncode)
Niestety jak tak zrobię to zespawnowany program nigdy się nie ubija, po prostu wisi.
Co ciekaw jak zakomentuję wczytanie stdout procesu:
import signal
import subprocess
from subprocess import PIPE
command = ["python", "-c", """
print("Started")
value = input()
print("received: " + value)
while True:
pass
"""]
process = subprocess.Popen(command, stdout=PIPE, stdin=PIPE, stderr=PIPE)
#print("read line: " + process.stdout.readline().decode())
process.stdin.write(b"input\n")
process.stdin.flush()
process.send_signal(signal.CTRL_BREAK_EVENT)
process.send_signal(signal.CTRL_C_EVENT)
print(process.communicate())
print("Process finished with exit code:", process.returncode)
To wtedy proces od razu się ubija i nie udaje mi się wczytać outputu:
(b'', b'')
Process finished with exit code: 3221225794
Myślałem też, że możeby nie odpalać tego programu w osobnym procesie, tylko w osobnym wątku, dodać lock, i zwolnić go w odpowiednim momencie żeby odczytać stacktrace.
Co do rozwiązania, chciałbym uniknąć sleep()
- z prostego powodu, takiemu sleep()
trzeba nadać jakąś liczbę sekund do czekania, która może być odpowiednia na różnych maszynach, a to znaczy że są potencjalne raceconditiony - chyba że się je jakoś ogarnie, np sleep()
w while
'u, wtedy okej.
Pytanko mam więc takie: macie jakiś pomysł jak zespawnować proces, wysłać do niego interrupta i odczytać stacktrace?
Wiem że jest to możliwe, bo raz jak się bawiłem w debugerze to dostałem stacktrace spowodowany przez os.kill()
, ale nie udało mi się tego powtórzyć poza debuggerem.