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'
)
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]:
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.
username = 'scott',
machine = 'cheetah.house',
weight = 14,
- count = 4,
+ count = 6,
),
)
if self.ping('video.house'):
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,
),
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(
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,
),
)
"""Utilities related to user input."""
+import logging
import signal
import sys
from typing import List
import exceptions
+logger = logging.getLogger(__file__)
+
+
def single_keystroke_response(
valid_responses: List[str],
*,
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:
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
import enum
import logging
import re
+import sys
from typing import Dict, List
# Note: this module is fairly early loaded. Be aware of dependencies.
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(
- )
- self.parse_raw_macs_file(raw, Location.CABIN)
+ try:
+ raw = cmd_with_timeout(
+ 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
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:
'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:
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'
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:
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}")
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()
"""Utilities for dealing with "text"."""
from collections import defaultdict
+import logging
import math
import sys
from typing import List, NamedTuple, Optional
from ansi import fg, reset
+logger = logging.getLogger(__file__)
+
+
class RowsColumns(NamedTuple):
rows: int
columns: int
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))