Repository navigation
Expand file tree
/
Copy pathMasterConsoleLog.py
More file actions
228 lines (166 loc) · 6.49 KB
/
Copy pathMasterConsoleLog.py
File metadata and controls
228 lines (166 loc) · 6.49 KB
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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
#######################################################
# tMasterConsoleLog - Tee stdout and stderr to MasterConsole.log
#
# Keeps normal terminal output while also appending Python-level console output
# to a daily text log file.
#
import atexit
import sys
from datetime import datetime
from PySide6.QtCore import QObject, Signal
from DailyTextLogFile import tDailyTextLogFile
##########################################################################################
##########################################################################################
##########################################################################################
# tConsoleLineEmitter
#
# Carries each completed console line to whoever wants to display it - in practice the main
# window's Log box. This exists because the tee is a plain stream object while Qt widgets may
# only be touched from the GUI thread, and write() can be called from any thread. Connect
# LineWritten with Qt.QueuedConnection and the marshalling is handled for you.
#
class tConsoleLineEmitter(QObject):
LineWritten = Signal(str) # one complete console line, already timestamped
###############################################
# Constructor
#
def __init__(self):
super().__init__()
self._bEmitting = False
###############################################
# EmitLine - Publish one line, refusing to re-enter
#
# A slot that printed would come straight back through the tee. Under a direct connection that
# is unbounded recursion, so drop the nested line rather than take the console down with it.
#
def EmitLine(self, Line):
if self._bEmitting:
return
self._bEmitting = True
try:
self.LineWritten.emit(Line)
finally:
self._bEmitting = False
##########################################################################################
##########################################################################################
##########################################################################################
# tTeeStream
#
#
class tTeeStream:
###############################################
# Constructor
#
def __init__(self, OriginalStream, LogFile, LineEmitter=None):
self.OriginalStream = OriginalStream
self.LogFile = LogFile
self.LineEmitter = LineEmitter
self._bAtLineStart = True
self._LineBuffer = ''
###############################################
# write - Write text to the terminal stream and the timestamped log file
#
# Only the log file copy is stamped, so the terminal looks exactly as it always has. The date
# is deliberately omitted - the log rotates daily, so time of day is unambiguous, and short
# lines stay readable. Milliseconds are included because correlating a USB dropout with the
# relay operation that may have caused it needs sub-second resolution.
#
def write(self, Text):
self.OriginalStream.write(Text)
StampedText = self._StampLines(Text)
self.LogFile.WriteText(StampedText)
self._EmitCompleteLines(StampedText)
return len(Text)
###############################################
# _EmitCompleteLines - Hand finished lines to the line emitter
#
# The GUI box wants whole lines, not the fragments print() delivers, so hold a partial line
# back until its newline arrives. The text is already stamped, so the Log box and
# MasterConsole.log read identically.
#
def _EmitCompleteLines(self, StampedText):
if self.LineEmitter is None:
return
self._LineBuffer += StampedText
while '\n' in self._LineBuffer:
Line, self._LineBuffer = self._LineBuffer.split('\n', 1)
self.LineEmitter.EmitLine(Line)
###############################################
# _StampLines - Prefix every line in Text with the time of day
#
# print() delivers one line across several write() calls (the text, then the newline), so a
# stamp may only be emitted at a real line start. _bAtLineStart carries that across calls.
#
def _StampLines(self, Text):
if not Text:
return Text
Stamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] + ' '
Output = []
for Character in Text:
if self._bAtLineStart:
Output.append(Stamp)
self._bAtLineStart = False
Output.append(Character)
if Character == '\n':
self._bAtLineStart = True
return ''.join(Output)
###############################################
# flush - Flush the terminal stream
#
def flush(self):
self.OriginalStream.flush()
###############################################
# __getattr__ - Delegate stream attributes to the original stream
#
def __getattr__(self, Name):
return getattr(self.OriginalStream, Name)
##########################################################################################
##########################################################################################
##########################################################################################
# tMasterConsoleLog
#
#
class tMasterConsoleLog:
###############################################
# Constructor
#
def __init__(self, DailyFolder):
self.SysModule = sys
self.LogFile = tDailyTextLogFile(DailyFolder, "MasterConsole.log")
self.OriginalStdout = self.SysModule.stdout
self.OriginalStderr = self.SysModule.stderr
self.LineEmitter = tConsoleLineEmitter()
self.bInstalled = False
self.Install()
atexit.register(self.Restore)
###############################################
# Install - Replace stdout and stderr with tee streams
#
def Install(self):
if self.bInstalled:
return
self.SysModule.stdout = tTeeStream(self.OriginalStdout, self.LogFile, self.LineEmitter)
self.SysModule.stderr = tTeeStream(self.OriginalStderr, self.LogFile, self.LineEmitter)
self.bInstalled = True
###############################################
# Restore - Restore stdout and stderr to their original streams
#
def Restore(self):
if not self.bInstalled:
return
CurrentStdout = self.SysModule.stdout
CurrentStderr = self.SysModule.stderr
for Stream in (CurrentStdout, CurrentStderr):
try:
if Stream is not None:
Stream.flush()
except Exception:
pass
self.SysModule.stdout = self.OriginalStdout
self.SysModule.stderr = self.OriginalStderr
self.bInstalled = False
###############################################
# StartNewFile - Rotate MasterConsole.log
#
def StartNewFile(self):
return self.LogFile.StartNewFile()