wasatch 2.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wasatch/AbstractUSBDevice.py +29 -0
- wasatch/AndorDevice.py +910 -0
- wasatch/AreaScanImage.py +19 -0
- wasatch/AutoRaman.py +457 -0
- wasatch/AutoRamanRequest.py +96 -0
- wasatch/BLEDevice.py +1032 -0
- wasatch/BalanceAcquisition.py +136 -0
- wasatch/CSVLoader.py +188 -0
- wasatch/CommandSettings.py +95 -0
- wasatch/ControlObject.py +17 -0
- wasatch/DetectorROI.py +36 -0
- wasatch/DetectorRegions.py +158 -0
- wasatch/DeviceFinderBLE.py +107 -0
- wasatch/DeviceFinderTCP.py +20 -0
- wasatch/DeviceFinderUSB.py +236 -0
- wasatch/DeviceID.py +344 -0
- wasatch/EEPROM.py +1450 -0
- wasatch/EEPROM_multical.sav +48 -0
- wasatch/FPGAOptions.py +125 -0
- wasatch/FeatureIdentificationDevice.py +3961 -0
- wasatch/FirmwareRequirements.py +76 -0
- wasatch/HardwareInfo.py +36 -0
- wasatch/IDSCamera.py +916 -0
- wasatch/IDSDevice.py +329 -0
- wasatch/IMX385.py +179 -0
- wasatch/InterfaceDevice.py +40 -0
- wasatch/MockUSBDevice.py +353 -0
- wasatch/OceanDevice.py +215 -0
- wasatch/PollStatus.py +15 -0
- wasatch/ProcessedReading.py +352 -0
- wasatch/ROI.py +33 -0
- wasatch/Reading.py +80 -0
- wasatch/RealUSBDevice.py +64 -0
- wasatch/SPIDevice.py +628 -0
- wasatch/SpectrometerRequest.py +15 -0
- wasatch/SpectrometerResponse.py +38 -0
- wasatch/SpectrometerSettings.py +446 -0
- wasatch/SpectrometerState.py +255 -0
- wasatch/StatusMessage.py +14 -0
- wasatch/TCPDevice.py +409 -0
- wasatch/TakeOneRequest.py +43 -0
- wasatch/WasatchBus.py +87 -0
- wasatch/WasatchDevice.py +1060 -0
- wasatch/WasatchDeviceWrapper.py +452 -0
- wasatch/WrapperWorker.py +304 -0
- wasatch/__init__.py +4 -0
- wasatch/applog.py +177 -0
- wasatch/simulation_protocol.py +227 -0
- wasatch/utils.py +731 -0
- wasatch-2.3.2.dist-info/LICENSE +21 -0
- wasatch-2.3.2.dist-info/METADATA +347 -0
- wasatch-2.3.2.dist-info/RECORD +53 -0
- wasatch-2.3.2.dist-info/WHEEL +4 -0
wasatch/WrapperWorker.py
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import platform
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from queue import Queue
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
from .SpectrometerResponse import SpectrometerResponse
|
|
9
|
+
from .SpectrometerRequest import SpectrometerRequest
|
|
10
|
+
from .WasatchDevice import WasatchDevice
|
|
11
|
+
from .AndorDevice import AndorDevice
|
|
12
|
+
from .OceanDevice import OceanDevice
|
|
13
|
+
from .SPIDevice import SPIDevice
|
|
14
|
+
from .BLEDevice import BLEDevice
|
|
15
|
+
from .TCPDevice import TCPDevice
|
|
16
|
+
from .Reading import Reading
|
|
17
|
+
|
|
18
|
+
if "windows" in platform.platform().lower():
|
|
19
|
+
from .IDSDevice import IDSDevice
|
|
20
|
+
|
|
21
|
+
log = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
##
|
|
24
|
+
# Continuously process in background thread. While waiting forever for the None
|
|
25
|
+
# poison pill on the command queue, continuously read from the device and post
|
|
26
|
+
# the results on the response queue.
|
|
27
|
+
#
|
|
28
|
+
# Consider moving this class, and WasatchDeviceWrapper, our of Wasatch.PY and
|
|
29
|
+
# into ENLIGHTEN. Let Wasatch.PY be a simple single-spectrometer blocking
|
|
30
|
+
# driver, similar to Wasatch.NET and Wasatch.VCPP. No need to have Wasatch.PY
|
|
31
|
+
# handle the threads.
|
|
32
|
+
class WrapperWorker(threading.Thread):
|
|
33
|
+
|
|
34
|
+
# TODO: make this dynamic:
|
|
35
|
+
# - initially on number of connected spectrometers
|
|
36
|
+
# - ideally on configured integration times per spectrometer
|
|
37
|
+
# - note that this is essentially ADDED to the total measurement time
|
|
38
|
+
# of EACH AND EVERY INTEGRATION
|
|
39
|
+
# TODO: replace if check for each type of spec with single call
|
|
40
|
+
# TODO: Create ABC of hardware device that keeps common functions like handle_requests
|
|
41
|
+
POLLER_WAIT_SEC = 0.05 # .05sec = 50ms = update from hardware device at 20Hz
|
|
42
|
+
|
|
43
|
+
DEBUG_SEC = 10 # enforce debug logging for the 1st 10sec after connecting a new spectrometer
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
device_id,
|
|
48
|
+
command_queue,
|
|
49
|
+
response_queue,
|
|
50
|
+
settings_queue,
|
|
51
|
+
message_queue,
|
|
52
|
+
is_ocean, # MZ: all these bools should have just been a string "type"
|
|
53
|
+
is_andor,
|
|
54
|
+
is_spi,
|
|
55
|
+
is_ble,
|
|
56
|
+
is_tcp,
|
|
57
|
+
is_ids,
|
|
58
|
+
log_level,
|
|
59
|
+
callback=None,
|
|
60
|
+
alert_queue=None):
|
|
61
|
+
|
|
62
|
+
threading.Thread.__init__(self)
|
|
63
|
+
|
|
64
|
+
self.device_id = device_id
|
|
65
|
+
self.is_ocean = is_ocean
|
|
66
|
+
self.is_andor = is_andor
|
|
67
|
+
self.is_spi = is_spi
|
|
68
|
+
self.is_ble = is_ble
|
|
69
|
+
self.is_tcp = is_tcp
|
|
70
|
+
self.is_ids = is_ids
|
|
71
|
+
self.command_queue = command_queue
|
|
72
|
+
self.response_queue = response_queue
|
|
73
|
+
self.settings_queue = settings_queue
|
|
74
|
+
self.message_queue = message_queue
|
|
75
|
+
self.alert_queue = alert_queue
|
|
76
|
+
self.log_level = log_level
|
|
77
|
+
self.callback = callback
|
|
78
|
+
|
|
79
|
+
self.connected_device = None
|
|
80
|
+
|
|
81
|
+
self.thread_start = datetime.now()
|
|
82
|
+
self.initial_connection_logging = True
|
|
83
|
+
|
|
84
|
+
# enforce debug logging around ENLIGHTEN connections
|
|
85
|
+
if not self.callback:
|
|
86
|
+
logging.getLogger().setLevel("DEBUG")
|
|
87
|
+
|
|
88
|
+
##
|
|
89
|
+
# This is essentially the main() loop in a thread.
|
|
90
|
+
# All communications with the parent thread are routed through
|
|
91
|
+
# one of the three queues (cmd inputs, response outputs, and
|
|
92
|
+
# a one-shot SpectrometerSettings).
|
|
93
|
+
def run(self):
|
|
94
|
+
if "windows" in platform.platform().lower():
|
|
95
|
+
is_options = (self.is_ocean, self.is_andor, self.is_ble, self.is_spi, self.is_tcp, self.is_ids)
|
|
96
|
+
device_classes = (OceanDevice, AndorDevice, BLEDevice, SPIDevice, TCPDevice, IDSDevice, WasatchDevice)
|
|
97
|
+
else:
|
|
98
|
+
is_options = (self.is_ocean, self.is_andor, self.is_ble, self.is_spi, self.is_tcp)
|
|
99
|
+
device_classes = (OceanDevice, AndorDevice, BLEDevice, SPIDevice, TCPDevice, WasatchDevice)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
if any(is_options):
|
|
103
|
+
type_connection = is_options.index(True)
|
|
104
|
+
log.debug(f"trying to instantiate device of type {device_classes[type_connection]}")
|
|
105
|
+
self.connected_device = device_classes[type_connection](
|
|
106
|
+
device_id = self.device_id,
|
|
107
|
+
message_queue = self.message_queue,
|
|
108
|
+
alert_queue = self.alert_queue)
|
|
109
|
+
else:
|
|
110
|
+
log.debug(f"Couldn't recognize device of {self.device_id} {is_options}, trying to instantiate as WasatchDevice")
|
|
111
|
+
self.connected_device = device_classes[device_classes.index(WasatchDevice)](
|
|
112
|
+
device_id = self.device_id,
|
|
113
|
+
message_queue = self.message_queue,
|
|
114
|
+
alert_queue = self.alert_queue)
|
|
115
|
+
except:
|
|
116
|
+
log.critical("exception instantiating device", exc_info=1)
|
|
117
|
+
return self.settings_queue.put(None)
|
|
118
|
+
|
|
119
|
+
log.debug("calling connect")
|
|
120
|
+
ok = False
|
|
121
|
+
req = SpectrometerRequest("connect")
|
|
122
|
+
try:
|
|
123
|
+
(ok,) = self.connected_device.handle_requests([req])
|
|
124
|
+
except:
|
|
125
|
+
log.critical("exception connecting", exc_info=1)
|
|
126
|
+
return self.settings_queue.put_nowait(SpectrometerResponse(error_msg="exception while connecting"))
|
|
127
|
+
|
|
128
|
+
log.debug(f"on connect request got results of {ok}")
|
|
129
|
+
|
|
130
|
+
if not ok.data:
|
|
131
|
+
log.critical("failed to connect")
|
|
132
|
+
return self.settings_queue.put_nowait(ok)
|
|
133
|
+
|
|
134
|
+
log.debug("successfully connected")
|
|
135
|
+
|
|
136
|
+
# send the SpectrometerSettings back to the GUI thread
|
|
137
|
+
log.debug("returning SpectrometerSettings to parent via SpectrometerResponse")
|
|
138
|
+
self.settings_queue.put_nowait(SpectrometerResponse(self.connected_device.settings))
|
|
139
|
+
|
|
140
|
+
log.debug("entering loop")
|
|
141
|
+
received_poison_pill_command = False # from ENLIGHTEN
|
|
142
|
+
|
|
143
|
+
num_connected_devices = 1
|
|
144
|
+
while True:
|
|
145
|
+
dedupped = self.dedupe(self.command_queue)
|
|
146
|
+
|
|
147
|
+
# apply dedupped commands
|
|
148
|
+
if dedupped:
|
|
149
|
+
for record in dedupped:
|
|
150
|
+
if record is None:
|
|
151
|
+
# We have received a "poison pill" (shutdown command)
|
|
152
|
+
# from ENLIGHTEN.
|
|
153
|
+
#
|
|
154
|
+
# Reminder, poison_pills moving DOWNSTREAM from ENLIGHTEN
|
|
155
|
+
# are None, while poison_pills moving UPSTREAM from WasatchDevice
|
|
156
|
+
# are indicated with SpectrometerResponse.poison_pill.
|
|
157
|
+
received_poison_pill_command = True
|
|
158
|
+
#
|
|
159
|
+
# Do NOT put a 'break' here just yet -- if caller is in process of
|
|
160
|
+
# cleaning shutting things down, let them switch off the
|
|
161
|
+
# laser etc in due sequence. We can break AFTER relaying
|
|
162
|
+
# applying the queued settings.
|
|
163
|
+
else:
|
|
164
|
+
try:
|
|
165
|
+
log.debug("processing command queue: %s", record.setting)
|
|
166
|
+
|
|
167
|
+
# basically, this simply moves each de-dupped command from
|
|
168
|
+
# WasatchDeviceWrapper.command_queue to WasatchDevice.command_queue,
|
|
169
|
+
# where it gets read during the next call to
|
|
170
|
+
# WasatchDevice.acquire_data.
|
|
171
|
+
if record.setting == "reset":
|
|
172
|
+
log.debug(f"calling reset from command queue")
|
|
173
|
+
req = SpectrometerRequest(record.setting, args=[record.value])
|
|
174
|
+
self.connected_device.handle_requests([req])
|
|
175
|
+
|
|
176
|
+
# peek in some settings locally
|
|
177
|
+
if record.setting == "num_connected_devices":
|
|
178
|
+
num_connected_devices = record.value
|
|
179
|
+
except:
|
|
180
|
+
log.error("failed to process record {record}, treating as poison pill", exc_info=1)
|
|
181
|
+
received_poison_pill_command = True
|
|
182
|
+
else:
|
|
183
|
+
log.debug("command queue empty")
|
|
184
|
+
|
|
185
|
+
if received_poison_pill_command:
|
|
186
|
+
# ...NOW we can break
|
|
187
|
+
log.critical("exiting per command queue (poison pill received)")
|
|
188
|
+
req = SpectrometerRequest("disconnect")
|
|
189
|
+
# I don't see this called. I think it should.
|
|
190
|
+
# At minimum it's required for the BLE devices
|
|
191
|
+
self.connected_device.handle_requests([req])
|
|
192
|
+
break
|
|
193
|
+
|
|
194
|
+
# ##################################################################
|
|
195
|
+
# Relay one upstream reading (Spectrometer -> GUI)
|
|
196
|
+
# ##################################################################
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
# Note: this is a BLOCKING CALL. If integration time is longer
|
|
200
|
+
# than subprocess_timeout_sec, this call itself will trigger
|
|
201
|
+
# shutdown.
|
|
202
|
+
log.debug("acquiring data")
|
|
203
|
+
req = SpectrometerRequest("acquire_data")
|
|
204
|
+
(reading_response,) = self.connected_device.handle_requests([req])
|
|
205
|
+
#log.debug("continuous_poll: acquire_data returned %s", str(reading))
|
|
206
|
+
except:
|
|
207
|
+
log.critical("exception calling WasatchDevice.acquire_data", exc_info=1)
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
if not isinstance(reading_response, SpectrometerResponse):
|
|
211
|
+
log.error(f"Reading is not type ReadingResponse. Should not get naked responses. Happened with request {req}")
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
log.debug(f"response {reading_response} data is {reading_response.data}")
|
|
215
|
+
|
|
216
|
+
if self.callback:
|
|
217
|
+
log.debug("worker returning response via callback")
|
|
218
|
+
self.callback(reading_response)
|
|
219
|
+
|
|
220
|
+
elif reading_response.keep_alive:
|
|
221
|
+
log.debug("worker is flowing up keep_alive")
|
|
222
|
+
self.response_queue.put(reading_response)
|
|
223
|
+
|
|
224
|
+
elif reading_response.error_msg != "":
|
|
225
|
+
if reading_response.data is None:
|
|
226
|
+
reading_response.data = Reading()
|
|
227
|
+
self.response_queue.put(reading_response)
|
|
228
|
+
|
|
229
|
+
elif reading_response.data is None:
|
|
230
|
+
log.debug("worker saw no reading (but not error, either)")
|
|
231
|
+
|
|
232
|
+
elif not reading_response.data:
|
|
233
|
+
log.critical(f"hardware level error...exiting because data False")
|
|
234
|
+
reading_response.poison_pill = True
|
|
235
|
+
self.response_queue.put(reading_response)
|
|
236
|
+
|
|
237
|
+
elif reading_response.data.failure is not None:
|
|
238
|
+
log.critical(f"hardware level error...exiting because failure {reading_response.data.failure}")
|
|
239
|
+
reading_response.poison_pill = True
|
|
240
|
+
self.response_queue.put(reading_response)
|
|
241
|
+
|
|
242
|
+
elif reading_response.poison_pill:
|
|
243
|
+
log.critical(f"hardware level error...exiting because poison-pill")
|
|
244
|
+
self.response_queue.put(reading_response)
|
|
245
|
+
|
|
246
|
+
elif reading_response.data.spectrum is not None:
|
|
247
|
+
log.debug("sending Reading %d back to GUI thread (%s)", reading_response.data.session_count, reading_response.data.spectrum[0:5])
|
|
248
|
+
try:
|
|
249
|
+
self.response_queue.put_nowait(reading_response)
|
|
250
|
+
except:
|
|
251
|
+
log.error("unable to push Reading %d to GUI", reading_response.data.session_count, exc_info=1)
|
|
252
|
+
|
|
253
|
+
# We just successfully sent a reading back to the main thread.
|
|
254
|
+
# After sending 10sec of such readings, we can disable the default
|
|
255
|
+
# DEBUG logging and revert to whatever the caller requested.
|
|
256
|
+
if (self.initial_connection_logging and
|
|
257
|
+
(datetime.now() - self.thread_start).total_seconds() > self.DEBUG_SEC):
|
|
258
|
+
log.info(f"relaxing log_level to {self.log_level} after {self.DEBUG_SEC}sec")
|
|
259
|
+
logging.getLogger().setLevel(self.log_level)
|
|
260
|
+
self.initial_connection_logging = False
|
|
261
|
+
|
|
262
|
+
else:
|
|
263
|
+
log.error("received non-failure Reading without spectrum...ignoring?")
|
|
264
|
+
|
|
265
|
+
# only poll hardware buses at 20Hz
|
|
266
|
+
sleep_sec = WrapperWorker.POLLER_WAIT_SEC * num_connected_devices
|
|
267
|
+
log.debug("sleeping %.2f sec", sleep_sec)
|
|
268
|
+
time.sleep(sleep_sec)
|
|
269
|
+
|
|
270
|
+
########################################################################
|
|
271
|
+
# we have exited the loop
|
|
272
|
+
########################################################################
|
|
273
|
+
|
|
274
|
+
if received_poison_pill_command:
|
|
275
|
+
log.critical("exiting because of downstream poison-pill command from ENLIGHTEN")
|
|
276
|
+
else:
|
|
277
|
+
log.critical("exiting for no reason?!")
|
|
278
|
+
|
|
279
|
+
log.critical("done")
|
|
280
|
+
|
|
281
|
+
def dedupe(self, q: Queue):
|
|
282
|
+
keep = [] # list, not a set, because we want to keep it ordered
|
|
283
|
+
try:
|
|
284
|
+
while True:
|
|
285
|
+
if not q.empty():
|
|
286
|
+
control_object = q.get_nowait()
|
|
287
|
+
|
|
288
|
+
# treat None elements (poison pills) same as everything else
|
|
289
|
+
setting = None if control_object is None else control_object.setting
|
|
290
|
+
|
|
291
|
+
# remove previous setting if duplicate
|
|
292
|
+
new_keep = []
|
|
293
|
+
for co in keep:
|
|
294
|
+
if co.setting != setting:
|
|
295
|
+
new_keep.append(co)
|
|
296
|
+
keep = new_keep
|
|
297
|
+
|
|
298
|
+
# append the setting to the de-dupped list and track index
|
|
299
|
+
keep.append(control_object)
|
|
300
|
+
else:
|
|
301
|
+
break
|
|
302
|
+
except:
|
|
303
|
+
log.error("failed to dedupe command queue", exc_info=1)
|
|
304
|
+
return keep
|
wasatch/__init__.py
ADDED
wasatch/applog.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
##
|
|
2
|
+
# Custom logging setup and helper functions.
|
|
3
|
+
#
|
|
4
|
+
# The general approach is that the control portion of the application
|
|
5
|
+
# instantiates a MainLogger object. This will create a separate process
|
|
6
|
+
# that looks for log events on a queue. Each process of the application then
|
|
7
|
+
# registers a queue handler, and writes its log events. The MainLogger loop
|
|
8
|
+
# will collect and write these log events to file and any other defined
|
|
9
|
+
# logging location.
|
|
10
|
+
#
|
|
11
|
+
# @see http://plumberjack.blogspot.com/2010/09/using-logging-with-multiprocessing.html
|
|
12
|
+
#
|
|
13
|
+
# The overall level of support for this file is basically "someone borrowed and
|
|
14
|
+
# modified it from an open-source example on the internet, and it basically works
|
|
15
|
+
# so I haven't putzed with it much" (except to try to understand when it breaks).
|
|
16
|
+
#
|
|
17
|
+
# @note on Windows, define PYTHONUTF8 environment variable to avoid error messages
|
|
18
|
+
# when log messages contain Unicode (default stdout/stderr streams are cp1252)
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
import logging
|
|
23
|
+
import platform
|
|
24
|
+
from . import utils
|
|
25
|
+
from queue import Queue
|
|
26
|
+
|
|
27
|
+
# ##############################################################################
|
|
28
|
+
# #
|
|
29
|
+
# Semi-static, module-level functions #
|
|
30
|
+
# #
|
|
31
|
+
# ##############################################################################
|
|
32
|
+
|
|
33
|
+
explicit_path = None
|
|
34
|
+
|
|
35
|
+
def set_location(path):
|
|
36
|
+
global explicit_path
|
|
37
|
+
explicit_path = path
|
|
38
|
+
|
|
39
|
+
def get_location():
|
|
40
|
+
global explicit_path
|
|
41
|
+
if explicit_path is not None:
|
|
42
|
+
return explicit_path
|
|
43
|
+
|
|
44
|
+
module_name = __name__.replace(".", "_") # "wasatch.applog" -> "wasatch_applog"
|
|
45
|
+
filename = "%s.txt" % module_name # "wasatch_applog.txt"
|
|
46
|
+
|
|
47
|
+
if "Linux" in platform.platform():
|
|
48
|
+
return filename
|
|
49
|
+
|
|
50
|
+
if "Darwin" in platform.system():
|
|
51
|
+
return filename
|
|
52
|
+
|
|
53
|
+
if "macOS" in platform.platform():
|
|
54
|
+
return filename
|
|
55
|
+
|
|
56
|
+
pathname = os.path.join("C:\\ProgramData", filename)
|
|
57
|
+
return pathname
|
|
58
|
+
|
|
59
|
+
##
|
|
60
|
+
# Called at the beginning of every subprocess, to tie into the existing
|
|
61
|
+
# root logger (owned and created by the MainProcess).
|
|
62
|
+
#
|
|
63
|
+
# Adds a queue handler object to the root logger to be processed in the
|
|
64
|
+
# main listener.
|
|
65
|
+
#
|
|
66
|
+
# Only on Windows though. Apparently Linux will pass the root logger
|
|
67
|
+
# amongst processes as expected, so if you add another queue handler you
|
|
68
|
+
# will get double log prints.
|
|
69
|
+
#
|
|
70
|
+
# Mimic the capturelog style of just slurping the entire log file contents.
|
|
71
|
+
#
|
|
72
|
+
# MZ: if we're just interested in the 'tail' of the log, this will be horribly
|
|
73
|
+
# inefficient for memory as the log file grows!
|
|
74
|
+
def get_text_from_log():
|
|
75
|
+
log_text = ""
|
|
76
|
+
with open(get_location()) as log_file:
|
|
77
|
+
# MZ: why not just 'return log_file.read()'?
|
|
78
|
+
for line_read in log_file:
|
|
79
|
+
log_text += line_read
|
|
80
|
+
return log_text
|
|
81
|
+
|
|
82
|
+
def log_file_created():
|
|
83
|
+
return os.path.exists(get_location())
|
|
84
|
+
|
|
85
|
+
## Remove the specified log file and return True if succesful.
|
|
86
|
+
def delete_log_file_if_exists():
|
|
87
|
+
pathname = get_location()
|
|
88
|
+
if os.path.exists(pathname):
|
|
89
|
+
os.remove(pathname)
|
|
90
|
+
return not os.path.exists(pathname)
|
|
91
|
+
|
|
92
|
+
def explicit_log_close():
|
|
93
|
+
root_log = logging.getLogger()
|
|
94
|
+
root_log.debug("applog.explicit_log_close: closing and removing all handlers")
|
|
95
|
+
handlers = root_log.handlers[:]
|
|
96
|
+
for handler in handlers:
|
|
97
|
+
handler.close()
|
|
98
|
+
root_log.removeHandler(handler)
|
|
99
|
+
|
|
100
|
+
# ##############################################################################
|
|
101
|
+
# #
|
|
102
|
+
# MainLogger #
|
|
103
|
+
# #
|
|
104
|
+
# ##############################################################################
|
|
105
|
+
|
|
106
|
+
class MainLogger:
|
|
107
|
+
FORMAT = u'%(asctime)s [0x%(thread)08x] %(name)s %(levelname)-8s %(message)s'
|
|
108
|
+
|
|
109
|
+
def __init__(self,
|
|
110
|
+
log_level=logging.DEBUG,
|
|
111
|
+
enable_stdout=True,
|
|
112
|
+
logfile=None,
|
|
113
|
+
timeout_sec=5,
|
|
114
|
+
append_arg="True"):
|
|
115
|
+
self.log_queue = Queue()
|
|
116
|
+
self.log_level = log_level
|
|
117
|
+
self.enable_stdout = enable_stdout
|
|
118
|
+
self.logfile = logfile
|
|
119
|
+
self.timeout_sec = timeout_sec
|
|
120
|
+
|
|
121
|
+
if self.logfile is not None:
|
|
122
|
+
set_location(self.logfile)
|
|
123
|
+
|
|
124
|
+
# append file size limits are enforced upon program restart
|
|
125
|
+
if append_arg.lower() == "true":
|
|
126
|
+
# limit to 300mb if --log-append is explicitly set to "True"
|
|
127
|
+
append = True
|
|
128
|
+
if append_arg.lower() == "false":
|
|
129
|
+
# when append is a falsy value, the log file is always reset on reboot
|
|
130
|
+
append = False
|
|
131
|
+
if append_arg.lower() == "limit":
|
|
132
|
+
# the default --log-append keeps up to 2mb between sessions
|
|
133
|
+
append = 2*1024*1024
|
|
134
|
+
|
|
135
|
+
root_log = logging.getLogger()
|
|
136
|
+
self.log_configurer(self.logfile, append)
|
|
137
|
+
root_log.setLevel(self.log_level)
|
|
138
|
+
root_log.warning("Top level log configuration (%d handlers, get_location %s)", len(root_log.handlers), get_location())
|
|
139
|
+
|
|
140
|
+
## Setup file handler and command window stream handlers. Every log
|
|
141
|
+
# message received on the queue handler will use these log configurers.
|
|
142
|
+
def log_configurer(self, logfile=None, append=False):
|
|
143
|
+
if logfile is not None:
|
|
144
|
+
pathname = logfile
|
|
145
|
+
elif self.logfile is not None:
|
|
146
|
+
pathname = self.logfile
|
|
147
|
+
else:
|
|
148
|
+
pathname = get_location()
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
if isinstance(append, int):
|
|
152
|
+
utils.resize_file(path=pathname, nbytes=append)
|
|
153
|
+
except (IOError, FileNotFoundError):
|
|
154
|
+
print("Unable to truncate log file.")
|
|
155
|
+
|
|
156
|
+
root_logger = logging.getLogger()
|
|
157
|
+
fh = logging.FileHandler(pathname, mode='a' if append else 'w', encoding='utf-8')
|
|
158
|
+
formatter = logging.Formatter(self.FORMAT)
|
|
159
|
+
fh.setFormatter(formatter)
|
|
160
|
+
root_logger.addHandler(fh)
|
|
161
|
+
|
|
162
|
+
if self.enable_stdout and sys.stdout is not None:
|
|
163
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
164
|
+
stream_handler = logging.StreamHandler(sys.stdout)
|
|
165
|
+
stream_handler.setFormatter(formatter)
|
|
166
|
+
root_logger.addHandler(stream_handler)
|
|
167
|
+
|
|
168
|
+
self.root = root_logger
|
|
169
|
+
|
|
170
|
+
## Wrapper to add a None poison pill to the listener process queue to
|
|
171
|
+
# ensure it exits.
|
|
172
|
+
def close(self):
|
|
173
|
+
self.log_queue.put_nowait(None)
|
|
174
|
+
try:
|
|
175
|
+
self.listener.join()
|
|
176
|
+
except:
|
|
177
|
+
pass
|