From b29be4f1750fd20bd2eada88e751dfae85817882 Mon Sep 17 00:00:00 2001 From: Scott Date: Sat, 4 Dec 2021 21:23:11 -0800 Subject: [PATCH] Various changes. --- arper.py | 43 ++++++++++++++++++++++++++++++++++--------- exec_utils.py | 14 ++++++++++++++ executors.py | 30 +++++++++++++++++++++++++----- input_utils.py | 8 ++++++++ presence.py | 19 ++++++++++++++----- remote_worker.py | 3 ++- smart_home/cameras.py | 4 ++-- smart_home/lights.py | 1 + string_utils.py | 21 ++++++++++++++++++++- text_utils.py | 15 +++++++++++++-- 10 files changed, 133 insertions(+), 25 deletions(-) diff --git a/arper.py b/arper.py index 4d6a3a2..2171e77 100644 --- a/arper.py +++ b/arper.py @@ -33,7 +33,7 @@ cfg.add_argument( cfg.add_argument( '--arper_cache_max_staleness', type=argparse_utils.valid_duration, - default=datetime.timedelta(seconds=60 * 60), + default=datetime.timedelta(seconds=60 * 15), metavar='DURATION', help='Max acceptable age of the kernel arp table cache' ) @@ -56,19 +56,44 @@ class Arper(persistent.Persistent): self.state = cached_state else: logger.debug('No usable cached state; calling /usr/sbin/arp') - self.update() + self.update_from_arp_scan() + self.update_from_arp() + if len(self.state) < config.config['arper_min_entries_to_be_valid']: + raise Exception('Arper didn\'t find enough entries; only got {len(self.state)}.') - def update(self): - output = exec_utils.cmd( - '/usr/sbin/arp -a', - timeout_seconds=5.0 - ) + def update_from_arp_scan(self): + network_spec = site_config.get_config().network + try: + output = exec_utils.cmd( + f'/usr/local/bin/arp-scan --retry=6 --timeout 350 --backoff=1.4 --random --numeric --plain --ignoredups {network_spec}', + timeout_seconds=10.0 + ) + except Exception as e: + logger.exception(e) + return + for line in output.split('\n'): + ip = string_utils.extract_ip_v4(line) + mac = string_utils.extract_mac_address(line) + if ip is not None and mac is not None and mac != 'UNKNOWN' and ip != 'UNKNOWN': + mac = mac.lower() + logger.debug(f'ARPER: {mac} => {ip}') + self.state[mac] = ip + + def update_from_arp(self): + try: + output = exec_utils.cmd( + '/usr/sbin/arp -a', + timeout_seconds=10.0 + ) + except Exception as e: + logger.exception(e) + return for line in output.split('\n'): ip = string_utils.extract_ip_v4(line) mac = string_utils.extract_mac_address(line) - if ip is not None and mac is not None: + if ip is not None and mac is not None and mac != 'UNKNOWN' and ip != 'UNKNOWN': mac = mac.lower() - logger.debug(f' {mac} => {ip}') + logger.debug(f'ARPER: {mac} => {ip}') self.state[mac] = ip def get_ip_by_mac(self, mac: str) -> Optional[str]: diff --git a/exec_utils.py b/exec_utils.py index 89cfbd7..b52f52f 100644 --- a/exec_utils.py +++ b/exec_utils.py @@ -10,6 +10,20 @@ from typing import List, Optional logger = logging.getLogger(__file__) +def cmd_showing_output(command: str) -> None: + p = subprocess.Popen( + command, + shell=True, + bufsize=0, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + for line in iter(p.stdout.readline, b''): + print(line.decode('utf-8'), end='') + p.stdout.close() + p.wait() + + def cmd_with_timeout(command: str, timeout_seconds: Optional[float]) -> int: """ Run a command but do not let it run for more than timeout seconds. diff --git a/executors.py b/executors.py index c11bd54..92c5b34 100644 --- a/executors.py +++ b/executors.py @@ -1113,7 +1113,7 @@ class DefaultExecutors(object): username = 'scott', machine = 'cheetah.house', weight = 14, - count = 4, + count = 6, ), ) if self.ping('video.house'): @@ -1126,12 +1126,12 @@ class DefaultExecutors(object): count = 4, ), ) - if self.ping('wannabe.house'): - logger.info('Found wannabe.house') + if self.ping('gorilla.house'): + logger.info('Found gorilla.house') pool.append( RemoteWorkerRecord( username = 'scott', - machine = 'wannabe.house', + machine = 'gorilla.house', weight = 2, count = 4, ), @@ -1156,6 +1156,16 @@ class DefaultExecutors(object): count = 2, ), ) + if self.ping('hero.house'): + logger.info('Found hero.house') + pool.append( + RemoteWorkerRecord( + username = 'scott', + machine = 'hero.house', + weight = 30, + count = 10, + ), + ) if self.ping('puma.cabin'): logger.info('Found puma.cabin') pool.append( @@ -1163,7 +1173,17 @@ class DefaultExecutors(object): username = 'scott', machine = 'puma.cabin', weight = 12, - count = 4, + count = 6, + ), + ) + if self.ping('puma.house'): + logger.info('Found puma.house') + pool.append( + RemoteWorkerRecord( + username = 'scott', + machine = 'puma.house', + weight = 12, + count = 6, ), ) diff --git a/input_utils.py b/input_utils.py index a989b2d..e0b457d 100644 --- a/input_utils.py +++ b/input_utils.py @@ -2,6 +2,7 @@ """Utilities related to user input.""" +import logging import signal import sys from typing import List @@ -11,6 +12,9 @@ import readchar # type: ignore import exceptions +logger = logging.getLogger(__file__) + + def single_keystroke_response( valid_responses: List[str], *, @@ -34,6 +38,7 @@ def single_keystroke_response( try: while True: response = readchar.readchar() + logger.debug(f'Keystroke: {ord(response)}') if response in valid_responses: break if ord(response) in os_special_keystrokes: @@ -50,6 +55,9 @@ def single_keystroke_response( response = _single_keystroke_response_internal( valid_responses, timeout_seconds ) + if ord(response) == 3: + raise KeyboardInterrupt('User pressed ^C in input_utils.') + except exceptions.TimeoutError: if default_response is not None: response = default_response diff --git a/presence.py b/presence.py index d7db416..5fad457 100755 --- a/presence.py +++ b/presence.py @@ -5,6 +5,7 @@ from collections import defaultdict import enum import logging import re +import sys from typing import Dict, List # Note: this module is fairly early loaded. Be aware of dependencies. @@ -81,16 +82,24 @@ class PresenceDetection(object): self.update() def update(self) -> None: - from exec_utils import cmd + from exec_utils import cmd_with_timeout try: persisted_macs = config.config['presence_macs_file'] except KeyError: persisted_macs = '/home/scott/cron/persisted_mac_addresses.txt' self.read_persisted_macs_file(persisted_macs, Location.HOUSE) - raw = cmd( - "ssh scott@meerkat.cabin 'cat /home/scott/cron/persisted_mac_addresses.txt'" - ) - self.parse_raw_macs_file(raw, Location.CABIN) + try: + raw = cmd_with_timeout( + "ssh scott@meerkat.cabin 'cat /home/scott/cron/persisted_mac_addresses.txt'", + timeout_seconds=10.0, + ) + self.parse_raw_macs_file(raw, Location.CABIN) + except Exception as e: + logger.exception(e) + logger.error( + 'Unable to fetch MAC Addresses from meerkat; can\'t do proper presence detection.' + ) + sys.exit(1) def read_persisted_macs_file( self, filename: str, location: Location diff --git a/remote_worker.py b/remote_worker.py index bf8de6c..c04ac65 100755 --- a/remote_worker.py +++ b/remote_worker.py @@ -83,7 +83,8 @@ def main() -> None: in_file = config.config['code_file'] out_file = config.config['result_file'] - (thread, stop_thread) = watch_for_cancel() + if config.config['watch_for_cancel']: + (thread, stop_thread) = watch_for_cancel() logger.debug(f'Reading {in_file}.') try: diff --git a/smart_home/cameras.py b/smart_home/cameras.py index 40850a9..8137012 100644 --- a/smart_home/cameras.py +++ b/smart_home/cameras.py @@ -15,7 +15,7 @@ class BaseCamera(dev.Device): 'outside_backyard_camera': 'backyard', 'outside_driveway_camera': 'driveway', 'outside_doorbell_camera': 'doorbell', - 'outside_front_door_camera': 'frontdoor', + 'outside_front_door_camera': 'front_door', } def __init__(self, name: str, mac: str, keywords: str = "") -> None: @@ -24,4 +24,4 @@ class BaseCamera(dev.Device): def get_stream_url(self) -> str: assert self.camera_name is not None - return f'http://10.0.0.56:81/mjpg/{self.camera_name}/video.mjpg?h=1024&q=99' + return f'http://10.0.0.226:8080/Umtxxf1uKMBniFblqeQ9KRbb6DDzN4/mp4/GKlT2FfiSQ/{self.camera_name}/s.mp4' diff --git a/smart_home/lights.py b/smart_home/lights.py index dd211eb..1c4081c 100644 --- a/smart_home/lights.py +++ b/smart_home/lights.py @@ -334,6 +334,7 @@ class TPLinkLight(BaseLight): def get_info(self) -> Optional[Dict]: cmd = self.get_cmdline() + "-c info" out = subprocess.getoutput(cmd) + logger.debug(f'RAW OUT> {out}') out = re.sub("Sent:.*\n", "", out) out = re.sub("Received: *", "", out) try: diff --git a/string_utils.py b/string_utils.py index 9a38d25..aca4a5e 100644 --- a/string_utils.py +++ b/string_utils.py @@ -1503,12 +1503,16 @@ def from_bitstring(bits: str, encoding='utf-8', errors='surrogatepass') -> str: return n.to_bytes((n.bit_length() + 7) // 8, 'big').decode(encoding, errors) or '\0' -def ip_v4_sort_key(txt: str) -> str: +def ip_v4_sort_key(txt: str) -> Tuple[int]: """Turn an IPv4 address into a tuple for sorting purposes. >>> ip_v4_sort_key('10.0.0.18') (10, 0, 0, 18) + >>> ips = ['10.0.0.10', '100.0.0.1', '1.2.3.4', '10.0.0.9'] + >>> sorted(ips, key=lambda x: ip_v4_sort_key(x)) + ['1.2.3.4', '10.0.0.9', '10.0.0.10', '100.0.0.1'] + """ if not is_ip_v4(txt): print(f"not IP: {txt}") @@ -1516,6 +1520,21 @@ def ip_v4_sort_key(txt: str) -> str: return tuple([int(x) for x in txt.split('.')]) +def path_ancestors_before_descendants_sort_key(volume: str) -> Tuple[str]: + """Chunk up a file path so that parent/ancestor paths sort before + children/descendant paths. + + >>> path_ancestors_before_descendants_sort_key('/usr/local/bin') + ('usr', 'local', 'bin') + + >>> paths = ['/usr/local', '/usr/local/bin', '/usr'] + >>> sorted(paths, key=lambda x: path_ancestors_before_descendants_sort_key(x)) + ['/usr', '/usr/local', '/usr/local/bin'] + + """ + return tuple([x for x in volume.split('/') if len(x) > 0]) + + if __name__ == '__main__': import doctest doctest.testmod() diff --git a/text_utils.py b/text_utils.py index 36cfe2f..9a9eb54 100644 --- a/text_utils.py +++ b/text_utils.py @@ -3,6 +3,7 @@ """Utilities for dealing with "text".""" from collections import defaultdict +import logging import math import sys from typing import List, NamedTuple, Optional @@ -10,6 +11,9 @@ from typing import List, NamedTuple, Optional from ansi import fg, reset +logger = logging.getLogger(__file__) + + class RowsColumns(NamedTuple): rows: int columns: int @@ -18,8 +22,15 @@ class RowsColumns(NamedTuple): def get_console_rows_columns() -> RowsColumns: """Returns the number of rows/columns on the current console.""" - from exec_utils import cmd - rows, columns = cmd("stty size").split() + from exec_utils import cmd_with_timeout + try: + rows, columns = cmd_with_timeout( + "stty size", + timeout_seconds=5.0, + ).split() + except Exception as e: + logger.exception(e) + raise Exception('Can\'t determine console size?!') return RowsColumns(int(rows), int(columns)) -- 2.47.1