Merge ~sylvain-pineau/checkbox-support:run_watcher_systemd into checkbox-support:master

Proposed by Sylvain Pineau
Status: Merged
Approved by: Sylvain Pineau
Approved revision: 139e4e61c6ec113cb8cabcf28f01bfca350e4eb7
Merged at revision: cff67a7d24f257fcfece504ee05b1658df1c9cba
Proposed branch: ~sylvain-pineau/checkbox-support:run_watcher_systemd
Merge into: checkbox-support:master
Diff against target: 683 lines (+158/-480)
2 files modified
checkbox_support/scripts/run_watcher.py (+158/-204)
dev/null (+0/-276)
Reviewer Review Type Date Requested Status
Jonathan Cave (community) Approve
Sylvain Pineau (community) Needs Resubmitting
Maciej Kisielewski Approve
Review via email: mp+353572@code.launchpad.net

Commit message

accessing syslog is not possible on UC18 and the same events can be found in systemd journal.

This MR updates the run watcher usb tool to rely on python3-systemd to still get the insert/remove events.

as part of this transition, the script now uses a class instead of globals.
log_watcher was only needed by this script so removed.

Tested on Desktop classic 18.04

To post a comment you must log in.
Revision history for this message
Maciej Kisielewski (kissiel) wrote :

Code looks good. +1
And I love (+158/-480).

Total nitpick would be that some not perfect code got copied. Like string interpolation in logger calls and using .warning() when stuff is an error.

review: Approve
Revision history for this message
Jonathan Cave (jocave) wrote :

I'm testing this on a gen2c board and the device insertion is not being detected. Would like to check the appropriate messages and being generated and searched for.

review: Needs Information
Revision history for this message
Sylvain Pineau (sylvain-pineau) wrote :

Fixed Maciej's nitpicks

For info, the parts update is here: https://code.launchpad.net/~sylvain-pineau/checkbox/+git/checkbox-dev-parts/+merge/353699

review: Needs Resubmitting
Revision history for this message
Jonathan Cave (jocave) wrote :

My findings are that the code works perfectly *if* the systemd libraries used by the checkbox snap match the version of systemd that is running the system journal.

i.e. if the device is an Ubuntu Core 18 device then the checkbox snap must have been built on bionic and target core18. If the device is a Ubuntu Core 16 device the snap must have been built on xenial

Therefore I see no problem with the code, but I think we need firm plan on how we are going to test devices before landing this.

Revision history for this message
Jonathan Cave (jocave) wrote :

We are now in a position to build snaps for different series so I think we can land this.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/checkbox_support/log_watcher.py b/checkbox_support/log_watcher.py
2deleted file mode 100644
3index 4ae2da5..0000000
4--- a/checkbox_support/log_watcher.py
5+++ /dev/null
6@@ -1,276 +0,0 @@
7-#!/usr/bin/env python3
8-"""
9-Real-time log files watcher supporting log rotation.
10-
11-Works with Python >= 2.6 and >= 3.2, on both POSIX and Windows.
12-
13-
14-License: MIT
15-
16-Original work Copyright (c) Giampaolo Rodola' <g.rodola [AT] gmail [DOT] com>
17-Modified work Copyright (c) 2015-2016: Taihsiang Ho <tai271828@gmail.com>
18-
19-Permission is hereby granted, free of charge, to any person obtaining a copy of
20-this software and associated documentation files (the "Software"), to deal in
21-the Software without restriction, including without limitation the rights to
22-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
23-of the Software, and to permit persons to whom the Software is furnished to do
24-so, subject to the following conditions:
25-
26-The above copyright notice and this permission notice shall be included in all
27-copies or substantial portions of the Software.
28-
29-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
31-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
32-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
33-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
34-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
35-SOFTWARE.
36-"""
37-import os
38-import time
39-import errno
40-import stat
41-
42-
43-class LogWatcher(object):
44-
45- """
46- Looks for changes in all files of a directory.
47-
48- This is useful for watching log file changes in real-time.
49- It also supports files rotation.
50-
51- Example:
52-
53- >>> def callback(filename, lines):
54- ... print(filename, lines)
55- ...
56- >>> lw = LogWatcher("/var/log/", callback)
57- >>> lw.loop()
58- """
59-
60- def __init__(self, folder: str, callback: 'Callable[[str], List[str]]',
61- extensions: "List[str]"=None, logfile: str=None,
62- tail_lines: int=0,
63- sizehint: int=1048576):
64- """
65- Initialize a new log watcher.
66-
67- :param folder: str
68- the folder to watch
69-
70- :param callback: callback
71- a function which is called every time one of the file being
72- watched is updated;
73- this is called with "filename" and "lines" arguments.
74-
75- :param extensions: list
76- only watch files with these extensions
77-
78- :param logfile: str
79- only watch this file. if this var exists,
80- it will override extention list above.
81-
82- :param tail_lines: int
83- read last N lines from files being watched before starting
84-
85- :param sizehint: int
86- passed to file.readlines(), represents an
87- approximation of the maximum number of bytes to read from
88- a file on every ieration (as opposed to load the entire
89- file in memory until EOF is reached). Defaults to 1MB.
90- """
91- self.folder = os.path.realpath(folder)
92- self.extensions = extensions
93- self.logfile = logfile
94- self._files_map = {}
95- self._callback = callback
96- self._sizehint = sizehint
97- assert os.path.isdir(self.folder), self.folder
98- assert callable(callback), repr(callback)
99- self.update_files()
100- for id, file in self._files_map.items():
101- file.seek(os.path.getsize(file.name)) # EOF
102- if tail_lines:
103- try:
104- lines = self.tail(file.name, tail_lines)
105- except IOError as err:
106- if err.errno != errno.ENOENT:
107- raise
108- else:
109- if lines:
110- self._callback(file.name, lines)
111-
112- def __enter__(self):
113- return self
114-
115- def __exit__(self, *args):
116- self.close()
117-
118- def __del__(self):
119- self.close()
120-
121- def loop(self, interval=0.1, blocking=True):
122- """Start a busy loop checking for file changes every *interval*
123- seconds. If *blocking* is False make one loop then return.
124- """
125- # May be overridden in order to use pyinotify lib and block
126- # until the directory being watched is updated.
127- # Note that directly calling readlines() as we do is faster
128- # than first checking file's last modification times.
129- while True:
130- self.update_files()
131- for fid, file in list(self._files_map.items()):
132- self.readlines(file)
133- if not blocking:
134- return
135- time.sleep(interval)
136-
137- def log(self, line):
138- """Log when a file is un/watched"""
139- print(line)
140-
141- def listdir(self):
142- """List directory and filter files by extension.
143- You may want to override this to add extra logic or globbing
144- support.
145- """
146- ls = os.listdir(self.folder)
147- if self.extensions:
148- ls = [x for x in ls if os.path.splitext(x)[1][1:]
149- in self.extensions]
150- if self.logfile in ls:
151- ls = [self.logfile]
152-
153- return ls
154-
155- @classmethod
156- def open(cls, file):
157- """Wrapper around open().
158- By default files are opened in binary mode and readlines()
159- will return bytes on both Python 2 and 3.
160- This means callback() will deal with a list of bytes.
161- Can be overridden in order to deal with unicode strings
162- instead, like this:
163-
164- import codecs, locale
165- return codecs.open(file, 'r', encoding=locale.getpreferredencoding(),
166- errors='ignore')
167- """
168- return open(file, 'rb')
169-
170- @classmethod
171- def tail(cls, fname, window):
172- """Read last N lines from file fname."""
173- if window <= 0:
174- raise ValueError('invalid window value %r' % window)
175- with cls.open(fname) as f:
176- buffer_size = 1024
177- # True if open() was overridden and file was opened in text
178- # mode. In that case readlines() will return unicode strings
179- # instead of bytes.
180- encoded = getattr(f, 'encoding', False)
181- CR = '\n' if encoded else b'\n'
182- data = '' if encoded else b''
183- f.seek(0, os.SEEK_END)
184- fsize = f.tell()
185- block = -1
186- exit = False
187- while not exit:
188- step = (block * buffer_size)
189- if abs(step) >= fsize:
190- f.seek(0)
191- newdata = f.read(buffer_size - (abs(step) - fsize))
192- exit = True
193- else:
194- f.seek(step, os.SEEK_END)
195- newdata = f.read(buffer_size)
196- data = newdata + data
197- if data.count(CR) >= window:
198- break
199- else:
200- block -= 1
201- return data.splitlines()[-window:]
202-
203- def update_files(self):
204- ls = []
205- for name in self.listdir():
206- absname = os.path.realpath(os.path.join(self.folder, name))
207- try:
208- st = os.stat(absname)
209- except EnvironmentError as err:
210- if err.errno != errno.ENOENT:
211- raise
212- else:
213- if not stat.S_ISREG(st.st_mode):
214- continue
215- fid = self.get_file_id(st)
216- ls.append((fid, absname))
217-
218- # check existent files
219- for fid, file in list(self._files_map.items()):
220- try:
221- st = os.stat(file.name)
222- except EnvironmentError as err:
223- if err.errno == errno.ENOENT:
224- self.unwatch(file, fid)
225- else:
226- raise
227- else:
228- if fid != self.get_file_id(st):
229- # same name but different file (rotation); reload it.
230- self.unwatch(file, fid)
231- self.watch(file.name)
232-
233- # add new ones
234- for fid, fname in ls:
235- if fid not in self._files_map:
236- self.watch(fname)
237-
238- def readlines(self, file):
239- """
240- Read file lines.
241-
242- Since last access until EOF is reached and invoke callback.
243- """
244- while True:
245- lines = file.readlines(self._sizehint)
246- if not lines:
247- break
248- self._callback(file.name, lines)
249-
250- def watch(self, fname):
251- try:
252- file = self.open(fname)
253- fid = self.get_file_id(os.stat(fname))
254- except EnvironmentError as err:
255- if err.errno != errno.ENOENT:
256- raise
257- else:
258- self.log("watching logfile %s" % fname)
259- self._files_map[fid] = file
260-
261- def unwatch(self, file, fid):
262- # File no longer exists. If it has been renamed try to read it
263- # for the last time in case we're dealing with a rotating log
264- # file.
265- self.log("un-watching logfile %s" % file.name)
266- del self._files_map[fid]
267- with file:
268- lines = self.readlines(file)
269- if lines:
270- self._callback(file.name, lines)
271-
272- @staticmethod
273- def get_file_id(st):
274- if os.name == 'posix':
275- return "%xg%x" % (st.st_dev, st.st_ino)
276- else:
277- return "%f" % st.st_ctime
278-
279- def close(self):
280- for id, file in self._files_map.items():
281- file.close()
282- self._files_map.clear()
283diff --git a/checkbox_support/scripts/run_watcher.py b/checkbox_support/scripts/run_watcher.py
284index 9283db6..24653a5 100644
285--- a/checkbox_support/scripts/run_watcher.py
286+++ b/checkbox_support/scripts/run_watcher.py
287@@ -1,219 +1,182 @@
288 #!/usr/bin/env python3
289-# Copyright 2015-2016 Canonical Ltd.
290+# Copyright 2015-2018 Canonical Ltd.
291 # All rights reserved.
292 #
293 # Written by:
294 # Taihsiang Ho <taihsiang.ho@canonical.com>
295+# Sylvain Pineau <sylvain.pineau@canonical.com>
296 """
297-application to use LogWatcher.
298-
299-this script use LogWatcher to define the actual behavior to watch log files by
300-a customized callback.
301+this script monitors the systemd journal to catch insert/removal USB events
302 """
303 import argparse
304 import contextlib
305-import sys
306+import logging
307 import os
308 import re
309+import select
310 import signal
311-import logging
312-from checkbox_support.log_watcher import LogWatcher
313-
314-global ARGS
315-USB_INSERT_TIMEOUT = 30 # sec
316-
317-# {log_string_1:status_1, log_string_2:status_2 ...}
318-FLAG_DETECTION = {"device": {
319- "new high-speed USB device number": False,
320- "new SuperSpeed USB device number": False
321- },
322- "driver": {
323- "using ehci_hcd": False,
324- "using xhci_hcd": False
325- },
326- "insertion": {
327- "USB Mass Storage device detected": False
328+import sys
329+from systemd import journal
330+
331+
332+logger = logging.getLogger(__file__)
333+logger.setLevel(logging.INFO)
334+logger.addHandler(logging.StreamHandler(sys.stdout))
335+
336+
337+class USBWatcher:
338+
339+ PART_RE = re.compile("sd\w+:.*(?P<part_name>sd\w+)")
340+ USB_ACTION_TIMEOUT = 30 # sec
341+ FLAG_DETECTION = {"device": {
342+ "new high-speed USB device number": False,
343+ "new SuperSpeed USB device number": False
344 },
345- "removal": {
346- "USB disconnect, device number": False
347+ "driver": {
348+ "using ehci_hcd": False,
349+ "using xhci_hcd": False
350+ },
351+ "insertion": {
352+ "USB Mass Storage device detected": False
353+ },
354+ "removal": {
355+ "USB disconnect, device number": False
356+ }
357 }
358- }
359-
360-
361-MOUNTED_PARTITION_CANDIDATES = None
362-MOUNTED_PARTITION = None
363-
364-logging.basicConfig(level=logging.WARNING)
365-
366-
367-######################################################
368-# run the log watcher
369-######################################################
370-
371-
372-def callback(filename, lines):
373- """
374- a callback function for LogWatcher.
375-
376- customized callback to define the actual behavior about how to watch and
377- what to watch of the log files.
378-
379- :param filename: str, a text filename. usually be a log file.
380- :param lines: list, contents the elements as string to tell what to watch.
381- """
382- for line in lines:
383- line_str = str(line)
384- refresh_detection(line_str)
385- get_partition_info(line_str)
386- report_detection()
387-
388-
389-def detect_str(line, str_2_detect):
390- """detect the string in the line."""
391- if str_2_detect in line:
392- return True
393- return False
394-
395-
396-def detect_partition(line):
397- """
398- detect device and partition info from lines.
399-
400- :param line:
401- str, line string from log file
402-
403- :return :
404- a list denoting [device, partition1, partition2 ...]
405- from syslog
406- """
407- # looking for string like
408- # sdb: sdb1
409- pattern = "sd.+sd.+"
410- match = re.search(pattern, line)
411- if match:
412- # remove the trailing \n and quote
413- match_string = match.group()[:-3]
414- # will looks like
415- # ['sdb', ' sdb1']
416- match_list = match_string.split(":")
417- return match_list
418-
419-
420-def get_partition_info(line_str):
421- """get partition info."""
422- global MOUNTED_PARTITION_CANDIDATES, MOUNTED_PARTITION
423- MOUNTED_PARTITION_CANDIDIATES = detect_partition(line_str)
424- if (MOUNTED_PARTITION_CANDIDIATES and
425- len(MOUNTED_PARTITION_CANDIDIATES) == 2):
426- # hard code because I expect
427- # FLAG_MOUNT_DEVICE_CANDIDIATES is something like ['sdb', ' sdb1']
428- # This should be smarter if the device has multiple partitions.
429- MOUNTED_PARTITION = MOUNTED_PARTITION_CANDIDIATES[1].strip()
430-
431-
432-def refresh_detection(line_str):
433- """
434- refresh values of the dictionary FLAG_DETECTION.
435-
436- :param line_str: str of the scanned log lines.
437- """
438- global FLAG_DETECTION
439- for key in FLAG_DETECTION.keys():
440- for sub_key in FLAG_DETECTION[key].keys():
441- if detect_str(line_str, sub_key):
442- FLAG_DETECTION[key][sub_key] = True
443-
444-
445-def report_detection():
446- """report detection status."""
447- # insertion detection
448- if (
449- ARGS.testcase == "insertion" and
450- FLAG_DETECTION["insertion"]["USB Mass Storage device detected"] and
451- MOUNTED_PARTITION
452- ):
453- device = ""
454- driver = ""
455- for key in FLAG_DETECTION["device"]:
456- if FLAG_DETECTION["device"][key]:
457- device = key
458- for key in FLAG_DETECTION["driver"]:
459- if FLAG_DETECTION["driver"][key]:
460- driver = key
461- logging.info("%s was inserted %s controller" % (device, driver))
462- logging.info("usable partition: %s" % MOUNTED_PARTITION)
463-
464- # judge the detection by the expection
465+
466+ def __init__(self, args):
467+ self.args = args
468+ self.MOUNTED_PARTITION = None
469+ signal.signal(signal.SIGALRM, self._no_usb_timeout)
470+ signal.alarm(self.USB_ACTION_TIMEOUT)
471+
472+ def run(self):
473+ j = journal.Reader()
474+ j.seek_tail()
475+ p = select.poll()
476+ p.register(j, j.get_events())
477+ if self.args.testcase == "insertion":
478+ print("\n\nINSERT NOW\n\n", flush=True)
479+ elif self.args.testcase == "removal":
480+ print("\n\nREMOVE NOW\n\n", flush=True)
481+ while p.poll():
482+ if j.process() != journal.APPEND:
483+ continue
484+ self._callback([e['MESSAGE'] for e in j if e])
485+
486+ def _callback(self, lines):
487+ for line in lines:
488+ line_str = str(line)
489+ self._refresh_detection(line_str)
490+ self._get_partition_info(line_str)
491+ self._report_detection()
492+
493+ def _get_partition_info(self, line_str):
494+ """get partition info."""
495+ # looking for string like "sdb: sdb1"
496+ match = re.search(self.PART_RE, line_str)
497+ if match:
498+ self.MOUNTED_PARTITION = match.group('part_name')
499+
500+ def _refresh_detection(self, line_str):
501+ """
502+ refresh values of the dictionary FLAG_DETECTION.
503+
504+ :param line_str: str of the scanned log lines.
505+ """
506+ for key in self.FLAG_DETECTION.keys():
507+ for sub_key in self.FLAG_DETECTION[key].keys():
508+ if sub_key in line_str:
509+ self.FLAG_DETECTION[key][sub_key] = True
510+
511+ def _report_detection(self):
512+ """report detection status."""
513+ # insertion detection
514 if (
515- ARGS.usb_type == 'usb2' and
516- device == "new high-speed USB device number"
517+ self.args.testcase == "insertion" and
518+ self.FLAG_DETECTION["insertion"]["USB Mass Storage device detected"] and
519+ self.MOUNTED_PARTITION
520 ):
521- print("USB2 insertion test passed.")
522- write_usb_info()
523- sys.exit()
524+ device = ""
525+ driver = ""
526+ for key in self.FLAG_DETECTION["device"]:
527+ if self.FLAG_DETECTION["device"][key]:
528+ device = key
529+ for key in self.FLAG_DETECTION["driver"]:
530+ if self.FLAG_DETECTION["driver"][key]:
531+ driver = key
532+ logger.info("%s was inserted %s controller" % (device, driver))
533+ logger.info("usable partition: %s" % self.MOUNTED_PARTITION)
534+ # judge the detection by the expection
535+ if (
536+ self.args.usb_type == 'usb2' and
537+ device == "new high-speed USB device number"
538+ ):
539+ logger.info("USB2 insertion test passed.")
540+ self._write_usb_info()
541+ sys.exit()
542+ if (
543+ self.args.usb_type == 'usb3' and
544+ device == "new SuperSpeed USB device number"
545+ ):
546+ logger.info("USB3 insertion test passed.")
547+ self._write_usb_info()
548+ sys.exit()
549+ # removal detection
550 if (
551- ARGS.usb_type == 'usb3' and
552- device == "new SuperSpeed USB device number"
553+ self.args.testcase == "removal" and
554+ self.FLAG_DETECTION["removal"]["USB disconnect, device number"]
555 ):
556- print("USB3 insertion test passed.")
557- write_usb_info()
558+ logger.info("Removal test passed.")
559+ self._remove_usb_info()
560 sys.exit()
561
562- # removal detection
563- if (
564- ARGS.testcase == "removal" and
565- FLAG_DETECTION["removal"]["USB disconnect, device number"]
566- ):
567- logging.info("An USB mass storage was removed.")
568- remove_usb_info()
569- sys.exit()
570-
571-
572-def write_usb_info():
573- """
574- reserve detected usb storage info.
575-
576- write the info we got in this script to $PLAINBOX_SESSION_SHARE
577- so the other jobs, e.g. read/write test, could know more information,
578- for example the partition it want to try to mount.
579- """
580- plainbox_session_share = os.environ.get('PLAINBOX_SESSION_SHARE')
581- if not plainbox_session_share:
582- logging.warning("no env var PLAINBOX_SESSION_SHARE")
583+ def _write_usb_info(self):
584+ """
585+ reserve detected usb storage info.
586+
587+ write the info we got in this script to $PLAINBOX_SESSION_SHARE
588+ so the other jobs, e.g. read/write test, could know more information,
589+ for example the partition it want to try to mount.
590+ """
591+ plainbox_session_share = os.environ.get('PLAINBOX_SESSION_SHARE')
592+ if not plainbox_session_share:
593+ logger.error("no env var PLAINBOX_SESSION_SHARE")
594+ sys.exit(1)
595+ if self.MOUNTED_PARTITION:
596+ logger.info(
597+ "cache file usb_insert_info is at: %s"
598+ % plainbox_session_share)
599+ file_to_share = open(
600+ os.path.join(plainbox_session_share, "usb_insert_info"), "w")
601+ file_to_share.write(self.MOUNTED_PARTITION + "\n")
602+ file_to_share.close()
603+
604+ def _remove_usb_info(self):
605+ """remove usb strage info from $PLAINBOX_SESSION_SHARE."""
606+ plainbox_session_share = os.environ.get('PLAINBOX_SESSION_SHARE')
607+ if not plainbox_session_share:
608+ logger.error("no env var PLAINBOX_SESSION_SHARE")
609+ sys.exit(1)
610+ file_to_share = os.path.join(
611+ plainbox_session_share, "usb_insert_info")
612+ with contextlib.suppress(FileNotFoundError):
613+ os.remove(file_to_share)
614+
615+ def _no_usb_timeout(self, signum, frame):
616+ """
617+ define timeout feature.
618+
619+ timeout and return failure if there is no usb insertion/removal
620+ detected after USB_ACTION_TIMEOUT secs
621+ """
622+ logger.error(
623+ "no USB storage %s was reported in systemd journal"
624+ % self.args.testcase)
625 sys.exit(1)
626
627- if MOUNTED_PARTITION:
628- logging.info(
629- "cache file usb_insert_info is at: %s" % plainbox_session_share)
630- file_to_share = open(
631- os.path.join(plainbox_session_share, "usb_insert_info"), "w")
632- file_to_share.write(MOUNTED_PARTITION + "\n")
633- file_to_share.close()
634-
635-
636-def remove_usb_info():
637- """remove usb strage info from $PLAINBOX_SESSION_SHARE."""
638- plainbox_session_share = os.environ.get('PLAINBOX_SESSION_SHARE')
639- if not plainbox_session_share:
640- logging.warning("no env var PLAINBOX_SESSION_SHARE")
641- sys.exit(1)
642- file_to_share = os.path.join(plainbox_session_share, "usb_insert_info")
643- with contextlib.suppress(FileNotFoundError):
644- os.remove(file_to_share)
645-
646-
647-def no_usb_timeout(signum, frame):
648- """
649- define timeout feature.
650-
651- timeout and return failure if there is no usb insertion is detected
652- after USB_INSERT_TIMEOUT secs
653- """
654- logging.info("no USB storage insertion was detected from /var/log/syslog")
655- sys.exit(1)
656
657 def main():
658- # access the parser
659 parser = argparse.ArgumentParser()
660 parser.add_argument('testcase',
661 choices=['insertion', 'removal'],
662@@ -221,18 +184,9 @@ def main():
663 parser.add_argument('usb_type',
664 choices=['usb2', 'usb3'],
665 help=("usb2 or usb3"))
666- global ARGS
667- ARGS = parser.parse_args()
668-
669- # set up the log watcher
670- watcher = LogWatcher("/var/log", callback, logfile="syslog")
671- signal.signal(signal.SIGALRM, no_usb_timeout)
672- signal.alarm(USB_INSERT_TIMEOUT)
673- if ARGS.testcase == "insertion":
674- print("\n\nINSERT NOW\n\n", flush=True)
675- elif ARGS.testcase == "removal":
676- print("\n\nREMOVE NOW\n\n", flush=True)
677- watcher.loop()
678+ args = parser.parse_args()
679+ watcher = USBWatcher(args)
680+ watcher.run()
681
682
683 if __name__ == "__main__":

Subscribers

People subscribed via source and target branches