|
rklm
Projektleitung
Anmeldungsdatum: 16. Oktober 2011
Beiträge: 13242
|
Manchmal lasse ich mehrere Prozesse im Hintergrund laufen und möchte, dass die Ausgabe aufgehoben und danach für jeden Hintergrundprozess einzeln abgespielt wird. Kennt Ihr ein Programm (oder Paar von Programmen), dass die Ausgaben eines anderen Prozesses nach Stdout und Stderr in eine Datei schreibt und im zweiten Schritt genau so wieder nach Stdout und Stderr abspielen kann? Hier ein Beispiel, wie ich mir die Nutzung vorstelle: | $ record -f outputs find * abc -type f # keine Ausgabe
...
$ replay outputs # jetzt Ausgabe nach Stdout und Stderr
bin/foo
bin/bar
find: ‘abc’: No such file or directory
$ replay outputs >/dev/null
find: ‘abc’: No such file or directory
|
Zeilen 4 und 5 sind normale Ausgaben nach Stdout und 6 und 8 sollen nach Stderr.
|
|
user_unknown
Anmeldungsdatum: 10. August 2005
Beiträge: 17630
|
record/replay hast Du Dir ausgedacht? In den Repos finde ich nur "recordmydesktop". Wenn es keine Aufzeichnung mit Pausen sein soll, die es in der Zeit abspielt, wie es gedauert hat, dann: | find * abc -type f > abc-find.log 2>&1
|
Ausgabe und Fehlermeldungen gehen in die gleiche Datei (abc-find.log). Seitenweise Ausgabe:
Zeilen 4 und 5 sind normale Ausgaben nach Stdout und 6 und 8 sollen nach Stderr.
Wenn Fehlermeldungen nicht ausgegeben, und daher auch nicht aufgezeichnet werden sollen:
| find * abc -type f > abc-find.log
|
ansonsten wie oben. Wenn Fehlermeldungen getrennt ausgegeben werden sollen: | find * abc -type f > abc-find.log 2> abc-find.err
less abc-find.log
less abc-find.err
|
Statt seitenweiser Ausgabe mit less kommt auch cat in Betracht, wird dann auf einen Rutsch ausgegeben.
|
|
shiro
Supporter
Anmeldungsdatum: 20. Juli 2020
Beiträge: 1303
|
Ich denke, es geht darum, alle Meldungen in der korrekten Sequenz in einem Logfile aufzunehmen und dann nach Bedarf wieder zu geben. Ich habe dies so gelöst, dass vor die jeweilige Zeile von "stdout" ein "o:" und vor die Zeile von "stderr" ein "e:" geschrieben wird. Beispiel:
$ touch file1.txt file2.txt
$ { ls -l file*txt file.noexists 2>&1 1>&3 3>&- | sed 's/^/e:/' >>file.log; } 3>&1 1>&2 | sed 's/^/o:/' >>file.log
$ cat file.log
e:ls: Zugriff auf 'file.noexists' nicht möglich: Datei oder Verzeichnis nicht gefunden
o:-rw-rw-r-- 1 shiro shiro 0 Jun 12 13:49 file1.txt
o:-rw-rw-r-- 1 shiro shiro 0 Jun 12 13:49 file2.txt
$
Man kann natürlich statt "o|e:" auch noch einen Timestamp einfügen. Also so was wie sed "s/^/$(date +%X) o:/". PS: Oder mit der Funktion "record":
$ mkdir bin
$ cd bin
bin$ touch foo bar
bin$ function record () { logfile=$1; [ -e $logfile ] && rm $logfile; shift; cmd=$@; { $cmd 2>&1 1>&3 3>&- | sed "s/^/$(date +%X) e:/" >>$logfile; } 3>&1 1>&2 | sed "s/^/$(date +%X) o:/" >>$logfile; }
bin$ record ../file.log find * abc -type f
bin$ cat ../file.log
14:13:26 e:find: ‘abc’: Datei oder Verzeichnis nicht gefunden
14:13:26 o:bar
14:13:26 o:foo
bin$
Auf "getopts" habe ich aus Faulheit verzichtet. Den Logfile muss man auch nicht zwingend löschen. PS2: Oops... Ich vergaß die "replay" Funktion. Hier ist sie:
$ function replay() { eo=${2:-o}; logfile=$1; grep "^[0-9:]* $eo:" $logfile;}
$ replay file.log
14:13:26 o:bar
14:13:26 o:foo
$ replay file.log e
14:13:26 e:find: ‘abc’: Datei oder Verzeichnis nicht gefunden
$
|
|
rklm
Projektleitung
(Themenstarter)
Anmeldungsdatum: 16. Oktober 2011
Beiträge: 13242
|
shiro schrieb: Ich denke, es geht darum, alle Meldungen in der korrekten Sequenz in einem Logfile aufzunehmen und dann nach Bedarf wieder zu geben.
Exakt.
Ich habe dies so gelöst, dass vor die jeweilige Zeile von "stdout" ein "o:" und vor die Zeile von "stderr" ein "e:" geschrieben wird. Beispiel:
Abgesehen davon, dass ich eine fertige Lösung suche, wird Deine hier vermutlich die Reihenfolge durch die Pufferung in sed verfälschen. Mindestens müsste man die Option --unbuffered verwenden.
$ touch file1.txt file2.txt
$ { ls -l file*txt file.noexists 2>&1 1>&3 3>&- | sed 's/^/e:/' >>file.log; } 3>&1 1>&2 | sed 's/^/o:/' >>file.log
$ cat file.log
e:ls: Zugriff auf 'file.noexists' nicht möglich: Datei oder Verzeichnis nicht gefunden
o:-rw-rw-r-- 1 shiro shiro 0 Jun 12 13:49 file1.txt
o:-rw-rw-r-- 1 shiro shiro 0 Jun 12 13:49 file2.txt
$
Danke dafür! Cooler Trick mit den Dateideskriptoren um zwei Pipes anzuhängen! 👍
|
|
Dakuan
Anmeldungsdatum: 2. November 2004
Beiträge: 6532
|
mir ist die Aufgabenstellung noch nicht ganz klar, und ich traue mich auch kaum zu fragen...
Kennt Ihr ein Programm (...), dass die Ausgaben eines anderen Prozesses nach Stdout und Stderr in eine Datei schreibt ...
Das klingt fast so, als sollten die Ausgaben nach dem Start der Programme abgegriffen werden, was ich mir ohne besondere Berechtigungen schwierig erscheint (ähnlich Keylogger). Es könnte aber auch sein, dass dies mit einem besonderen Starter möglich ist. Ich könnte mir vorstellen, dass das Startprogramm die Programme mit fork() startet. Dann hätte es Zugriff auf alle filedescriptoren. Eine Frage wäre dann, ob für jedes Programm eine eigene Logdatei erzeugt werden soll oder alles in eine Datei protokolliert werden soll (neu oder append). Das ist jetzt vielleicht etwas neben der Spur, aber da ich das Problem interessant finde, konnte ich meine Finger nicht still halten.
|
|
noisefloor
Anmeldungsdatum: 6. Juni 2006
Beiträge: 29567
|
Hallo, das sollte doch mit systemd/systemd-cat funktionieren, wenn man beim Schreiben ins Journal systemd-cat einen eigenen Identifier mit gib und dann mit journalctl gezielt den Identifier abfragt und ggf. noch den Zeitraum der Abfrage eingrenzt. Gruß, noisefloor
|
|
user_unknown
Anmeldungsdatum: 10. August 2005
Beiträge: 17630
|
Sorry, rklm, Ich hatte nicht drauf geachtet, wer die Frage stellt. Hätte ich drauf geachtet und gemerkt, dass Du fragst hätte ich die Frage wohl nicht falsch verstanden und etwas geantwortet, was Du sicher selbst weißt. Ich ziehe meine Antwort zurück. 😉
|
|
rklm
Projektleitung
(Themenstarter)
Anmeldungsdatum: 16. Oktober 2011
Beiträge: 13242
|
noisefloor schrieb:
das sollte doch mit systemd/systemd-cat funktionieren, wenn man beim Schreiben ins Journal systemd-cat einen eigenen Identifier mit gib und dann mit journalctl gezielt den Identifier abfragt und ggf. noch den Zeitraum der Abfrage eingrenzt.
Nein, die Ausgaben sollen nicht ins Journal. user_unknown schrieb: Sorry, rklm, Ich hatte nicht drauf geachtet, wer die Frage stellt. Hätte ich drauf geachtet und gemerkt, dass Du fragst hätte ich die Frage wohl nicht falsch verstanden und etwas geantwortet, was Du sicher selbst weißt. Ich ziehe meine Antwort zurück. 😉
🤣
|
|
rklm
Projektleitung
(Themenstarter)
Anmeldungsdatum: 16. Oktober 2011
Beiträge: 13242
|
Dakuan schrieb: mir ist die Aufgabenstellung noch nicht ganz klar, und ich traue mich auch kaum zu fragen...
Bin mir nicht sicher, wie ich es noch anders beschreiben soll: ich will alle Ausgaben eines Prozesses nach Stdout und Stderr aufheben und zwar so, dass ich sie nachher in der selben Reihenfolge wieder nach Stdout und Stderr ausgeben kann.
Das klingt fast so, als sollten die Ausgaben nach dem Start der Programme abgegriffen werden, was ich mir ohne besondere Berechtigungen schwierig erscheint (ähnlich Keylogger).
Nein, das ist überhaupt kein Problem. Im Prinzip passiert dasselbe wie bei einer Ausgabeumleitung - nur, dass hier zwei Datenströme so verschränkt sind, dass man sie nachher auch wieder trennen kann. Man braucht zwei Pipes und vielleicht auch ein Dateilock. Siehe den Vorschlag von shiro.
Es könnte aber auch sein, dass dies mit einem besonderen Starter möglich ist.
Darum geht es ja.
Ich könnte mir vorstellen, dass das Startprogramm die Programme mit fork() startet. Dann hätte es Zugriff auf alle filedescriptoren. Eine Frage wäre dann, ob für jedes Programm eine eigene Logdatei erzeugt werden soll oder alles in eine Datei protokolliert werden soll (neu oder append).
Eine Datei pro Prozess.
Das ist jetzt vielleicht etwas neben der Spur, aber da ich das Problem interessant finde, konnte ich meine Finger nicht still halten.
Kein Ding. Ich frage mich nur, ob ich mich irgendwie ungeschickt ausdrücke. Ein Anwendungsfall ist das, was parallel macht: dort werden die Ausgaben jedes Jobs zusammenhängend auf die Konsole geschrieben: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | $ time parallel 'for f in {1..5}; do echo {} $f; sleep 1; done' ::: aa bb
aa 1
aa 2
aa 3
aa 4
aa 5
bb 1
bb 2
bb 3
bb 4
bb 5
real 0m5,200s
user 0m0,189s
sys 0m0,038s
|
Man sieht, da wird parallel gearbeitet (Gesamtdauer ca. 5 Sekunden), aber die Ausgaben jedes Jobs werden zusammen ausgegeben. Um das zu ermöglichen muss man sie aufheben und danach sequentiell abspielen.
|
|
seahawk1986
Anmeldungsdatum: 27. Oktober 2006
Beiträge: 11278
|
script kann die Ein- und Ausgaben von Prozessen oder einer Shell-Session aufzeichnen - die Dateien kann man dann mit scriptreplay wiedergeben - aber das unterscheidet in der Aufzeichnung und Ausgabe nicht zwischen stdout und stderr - wenn ich das richtig verstehe, weil das Programm als Pseudo-TTY läuft, also nur die bereits kombinierten Streams abgreift - der Vorteil ist, dass dadurch Escape-Sequenzen mit aufgezeichnet werden. Ansonsten wäre das eine Lösung mit asyncio in Python3, solange man keine Features einer Shell bzw. eines Terminals braucht (https://blog.dalibo.com/2022/09/12/monitoring-python-subprocesses.html war da eine große Hilfe):
output_recorder.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130 | #!/usr/bin/env python3
import argparse
import asyncio
import datetime
import enum
import functools
import pickle
import sys
from typing import NamedTuple
class OutputType(enum.IntEnum):
STDOUT = 1
STDERR = 2
class ProtocolEntry(NamedTuple):
type: OutputType
relative_ts: float
text: bytes
class OutputProtocol(asyncio.subprocess.SubprocessStreamProtocol):
def __init__(
self,
stdout_reader: asyncio.StreamReader,
stderr_reader: asyncio.StreamReader,
limit,
loop,
):
super().__init__(limit=limit, loop=loop)
self._readers = {
1: stdout_reader,
2: stderr_reader,
}
def pipe_data_received(self, fd, data):
"""Called when the child process writes data into its stdout
or stderr pipe.
"""
super().pipe_data_received(fd, data)
if r := self._readers.get(fd):
r.feed_data(data)
def pipe_connection_lost(self, fd, exc):
"""Called when one of the pipes communicating with the child
process is closed.
"""
super().pipe_connection_lost(fd, exc)
if r := self._readers.get(fd):
if exc:
r.set_exception(exc)
else:
r.feed_eof()
async def main():
parser = argparse.ArgumentParser(
prog="Output Recorder",
description="Record and pickle the output of the executed program",
# epilog="Text at the bottom of help",
)
parser.add_argument("cmd", nargs="+", metavar="ARG", help="Argument(s) to execute")
parser.add_argument(
"-o",
"--output",
help="file to write the recorded output to",
default="output.pickle",
)
args = parser.parse_args()
cmd = args.cmd
loop = asyncio.get_event_loop()
stdout_reader = asyncio.StreamReader(loop=loop)
stderr_reader = asyncio.StreamReader(loop=loop)
combined_output: asyncio.Queue[ProtocolEntry | None] = asyncio.Queue()
protocol_factory = functools.partial(
OutputProtocol, stdout_reader, stderr_reader, limit=2**16, loop=loop
)
start_t = datetime.datetime.now()
async def log_stderr():
async for line in stderr_reader:
td = datetime.datetime.now() - start_t
sys.stderr.buffer.write(line)
sys.stderr.buffer.flush()
await combined_output.put(
ProtocolEntry(OutputType.STDERR, td.total_seconds(), line)
)
await combined_output.put(None)
async def log_stdout():
async for line in stdout_reader:
sys.stdout.buffer.write(line)
sys.stdout.buffer.flush()
td = datetime.datetime.now() - start_t
await combined_output.put(
ProtocolEntry(OutputType.STDOUT, td.total_seconds(), line)
)
await combined_output.put(None)
async def write_combined_output():
with open(args.output, "wb") as output:
n_streams_open = 2
while True:
entry = await combined_output.get()
if entry is None:
n_streams_open -= 1
if n_streams_open == 0:
break
pickle.dump(entry, output)
transport, protocol = await loop.subprocess_exec(
protocol_factory,
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
proc = asyncio.subprocess.Process(transport, protocol, loop)
r = await asyncio.gather(
proc.communicate(), log_stderr(), log_stdout(), write_combined_output()
)
if __name__ == "__main__":
asyncio.run(main())
|
Und die replay.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70 | #!/usr/bin/env python3
import asyncio
import argparse
import enum
import pickle
import typing
import sys
from typing import NamedTuple
class OutputType(enum.IntEnum):
STDOUT = 1
STDERR = 2
class ProtocolEntry(NamedTuple):
type: OutputType
relative_ts: float
text: bytes
PARSER = argparse.ArgumentParser(
prog="OutputReplay",
description="Replay the pickled output of output_logger",
# epilog="Text at the bottom of help",
)
PARSER.add_argument("filename")
PARSER.add_argument(
"-f", "--filter", choices=["stdout", "stderr", "all"], default="all"
)
PARSER.add_argument("-s", "--skip-waiting", action="store_true")
async def delayed_output_entry(entry: ProtocolEntry):
await asyncio.sleep(entry.relative_ts)
await output_entry(entry)
async def output_entry(entry: ProtocolEntry):
if entry.type == OutputType.STDERR:
sys.stderr.buffer.write(entry.text)
sys.stderr.buffer.flush()
elif entry.type == OutputType.STDOUT:
sys.stdout.buffer.write(entry.text)
sys.stdout.buffer.flush()
async def main():
args = PARSER.parse_args()
tasks: list[asyncio.Task] = []
output_method = output_entry if args.skip_waiting else delayed_output_entry
add_task = lambda entry: tasks.append(asyncio.create_task(output_method(entry)))
with open(args.filename, "rb") as fd:
while entry := pickle.load(fd):
entry = typing.cast(ProtocolEntry, entry)
if args.filter == "all":
add_task(entry)
elif args.filter == "stdout" and entry.type == OutputType.STDOUT:
add_task(entry)
elif args.filter == "stderr" and entry.type == OutputType.STDERR:
add_task(entry)
await asyncio.gather(*tasks)
if __name__ == "__main__":
asyncio.run(main())
|
Also z.B. mit einer test.sh, die folgenden Inhalt hat:
| #!/bin/bash
echo "Hallo, Welt"
sleep 1
echo "Error" > /dev/stderr
sleep 2
echo "Done"
|
Deren Ausgabe könne man so aufnehmen:
$ ./output_recorder.py -o test.pickle bash test.sh
Hallo, Welt
Error
Done
Und so wieder abspielen:
# Mit nachgeahmten Pausen zwischen den Ausgegebenen Zeilen:
$ ./replay.py test.pickle
Hallo, Welt
Error
Done
# Auf wunsch kann man filtern:
$ ./replay.py test_output.pickle -f "stderr"
Error
# bzw.
$ ./replay.py test.pickle > /dev/null
Error
# Und wenn es einem zu lange dauert, kann man die zeitlichen Pausen zwischen den einzelnen Zeilen überspringen:
$ ./replay.py test_output.pickle -f "stderr" -s
Error Edit: die Reihenfolge könnte bei einem Haufen von dicht gedrängten Ausgaben in der replay.py durcheinander kommen, das muss noch linearer gestalten.
|
|
Dakuan
Anmeldungsdatum: 2. November 2004
Beiträge: 6532
|
rklm schrieb: Ich frage mich nur, ob ich mich irgendwie ungeschickt ausdrücke.
Könnte auch sein, dass ich in die Frage zu viel hineininterpretiert hatte. Und mit dem Beispiel von shiro tue ich mich etwas schwer, da ich nicht so der Script Experte bin.
... und zwar so, dass ich sie nachher in der selben Reihenfolge wieder nach Stdout und Stderr ausgeben kann.
Das hatte ich so jetzt nicht mitgekriegt. Ich dachte zusammenfassen reicht.
|
|
rklm
Projektleitung
(Themenstarter)
Anmeldungsdatum: 16. Oktober 2011
Beiträge: 13242
|
Dakuan schrieb: rklm schrieb:
... und zwar so, dass ich sie nachher in der selben Reihenfolge wieder nach Stdout und Stderr ausgeben kann.
Das hatte ich so jetzt nicht mitgekriegt. Ich dachte zusammenfassen reicht.
Nein, das wäre ja einfach. 😛 Im Moment ist parallel immer noch das, was der Sache am nächsten kommt. Nur, dass man da selber keine Kontrolle über die Wiedergabe hat. parallel sorgt einfach dafür, dass die Ausgaben so kommen, wie das bei sequentieller Abarbeitung der Fall wäre.
|
|
Dakuan
Anmeldungsdatum: 2. November 2004
Beiträge: 6532
|
Nein, das wäre ja einfach. 😛
Die Antwort hatte ich jetzt fast erwartet. Ich habe jetzt mal weiter gesucht, aber nichts gefunden, was an deinen Wunsch herankommt. Aber ich habe etwas ähnliches gefunden, was zumindest mir weiter hilft. Ich verarbeite in einer GUI Anwendung die Ausgabe von ffprobe, ohne diese anzuzeigen. Fehlerausgaben möchte ich aber doch in einem eigenen Fenster anzeigen. Dazu habe ich folgendes gefunden:
In der zweiten Fundstelle ist ein Link zu github enthalten, der aber nicht (mehr?) funktioniert. Außerdem wurden in beiden Beiträgen einige Punkte bemängelt. Ist wohl nicht ganz trivial das Thema.
|
|
rklm
Projektleitung
(Themenstarter)
Anmeldungsdatum: 16. Oktober 2011
Beiträge: 13242
|
Danke! Für den Moment werde ich erst mal schauen, ob parallel reicht. Falls noch jemand was findet, immer her damit.
|