prim-ctrl 0.8.1__tar.gz → 0.8.2__tar.gz
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.
- {prim_ctrl-0.8.1 → prim_ctrl-0.8.2}/PKG-INFO +1 -1
- {prim_ctrl-0.8.1 → prim_ctrl-0.8.2}/prim_ctrl/__main__.py +34 -31
- {prim_ctrl-0.8.1 → prim_ctrl-0.8.2}/pyproject.toml +1 -1
- {prim_ctrl-0.8.1 → prim_ctrl-0.8.2}/LICENSE +0 -0
- {prim_ctrl-0.8.1 → prim_ctrl-0.8.2}/README.md +0 -0
- {prim_ctrl-0.8.1 → prim_ctrl-0.8.2}/prim_ctrl/__init__.py +0 -0
|
@@ -37,7 +37,7 @@ class LevelFormatter(logging.Formatter):
|
|
|
37
37
|
|
|
38
38
|
def __init__(self, fmts: dict[int, str], fmt: str, **kwargs):
|
|
39
39
|
super().__init__()
|
|
40
|
-
self.formatters =
|
|
40
|
+
self.formatters = {level: logging.Formatter(fmt, **kwargs) for level, fmt in fmts.items()}
|
|
41
41
|
self.default_formatter = logging.Formatter(fmt, **kwargs)
|
|
42
42
|
|
|
43
43
|
def format(self, record: logging.LogRecord) -> str:
|
|
@@ -132,7 +132,7 @@ class SignalFence():
|
|
|
132
132
|
if self.on_deferred_signal is not None:
|
|
133
133
|
try:
|
|
134
134
|
self.on_deferred_signal(signum, frame)
|
|
135
|
-
except:
|
|
135
|
+
except: # NOSONAR(S5754)
|
|
136
136
|
pass
|
|
137
137
|
|
|
138
138
|
def disable(self) -> None:
|
|
@@ -151,11 +151,11 @@ class SignalFence():
|
|
|
151
151
|
self.deferred_signal = None
|
|
152
152
|
logger.debug("Handling deferred signal %d", self.signum)
|
|
153
153
|
if isinstance(self.original_handler, signal.Handlers):
|
|
154
|
-
if self.original_handler
|
|
155
|
-
pass
|
|
156
|
-
elif self.original_handler is signal.Handlers.SIG_DFL:
|
|
154
|
+
if self.original_handler == signal.Handlers.SIG_DFL:
|
|
157
155
|
signal.signal(self.signum, signal.SIG_DFL)
|
|
158
156
|
os.kill(os.getpid(), self.signum)
|
|
157
|
+
# elif self.original_handler == signal.Handlers.SIG_IGN:
|
|
158
|
+
# pass
|
|
159
159
|
elif callable(self.original_handler):
|
|
160
160
|
self.original_handler(*deferred_signal)
|
|
161
161
|
|
|
@@ -193,7 +193,7 @@ class Subprocess:
|
|
|
193
193
|
# return result.returncode == 0
|
|
194
194
|
|
|
195
195
|
@staticmethod
|
|
196
|
-
async def ping(host, packets: int = 1, timeout: float = 1):
|
|
196
|
+
async def ping(host, packets: int = 1, timeout: float = 1): # NOSONAR(S7483)
|
|
197
197
|
if platform.system().lower() == 'windows':
|
|
198
198
|
command = ['ping', '-n', str(packets), '-w', str(int(timeout*1000)), host]
|
|
199
199
|
# don't use text=True, the async version will raise ValueError("text must be False"), who knows why
|
|
@@ -214,7 +214,7 @@ class Subprocess:
|
|
|
214
214
|
try:
|
|
215
215
|
proc = await asyncio.create_subprocess_exec(*command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, creationflags=creationflags)
|
|
216
216
|
except FileNotFoundError as e:
|
|
217
|
-
e.add_note(
|
|
217
|
+
e.add_note("Please check that Tailscale is installed properly")
|
|
218
218
|
raise
|
|
219
219
|
stdout, stderr = await proc.communicate()
|
|
220
220
|
return proc.returncode == 0, stdout.decode(), stderr.decode()
|
|
@@ -228,7 +228,7 @@ class ExternalDnsResolver(DnsResolver):
|
|
|
228
228
|
def __init__(self, where: str):
|
|
229
229
|
self.where = where
|
|
230
230
|
self.dns_resolver = None
|
|
231
|
-
self.cache
|
|
231
|
+
self.cache: dict[tuple[str, int, socket.AddressFamily], tuple[float, list[ResolveResult]]] = {}
|
|
232
232
|
|
|
233
233
|
async def resolve(self, host: str, port: int = 0, family: socket.AddressFamily = socket.AF_UNSPEC) -> list[ResolveResult]:
|
|
234
234
|
logger.debug("Resolving DNS at %s for %s:%i (%s)", self.where, host, port, "ipv6" if family == socket.AF_INET6 else "ipv4")
|
|
@@ -291,7 +291,7 @@ class Pingable(ABC):
|
|
|
291
291
|
def get_state_name(available: bool):
|
|
292
292
|
return 'up' if available else 'down'
|
|
293
293
|
|
|
294
|
-
async def wait_for(self, available: bool, timeout: float):
|
|
294
|
+
async def wait_for(self, available: bool, timeout: float): # NOSONAR(S7483)
|
|
295
295
|
logger.debug("Waiting for %s to be %s (timeout is %ds)", LazyStr(self.get_class_name), LazyStr(Pingable.get_state_name, available), int(timeout))
|
|
296
296
|
async with asyncio.timeout(timeout):
|
|
297
297
|
while await self.ping(available) != available:
|
|
@@ -315,7 +315,7 @@ class Manageable(Pingable):
|
|
|
315
315
|
super().__init__()
|
|
316
316
|
self.manager = manager
|
|
317
317
|
|
|
318
|
-
async def _set_state(self, available: bool, repeat: float, timeout: float):
|
|
318
|
+
async def _set_state(self, available: bool, repeat: float, timeout: float): # NOSONAR(S7483)
|
|
319
319
|
action_name = LazyStr(lambda: 'Starting' if available else 'Stopping')
|
|
320
320
|
class_name = LazyStr(self.get_class_name)
|
|
321
321
|
available_name = LazyStr(Pingable.get_state_name, available)
|
|
@@ -344,10 +344,10 @@ class Manageable(Pingable):
|
|
|
344
344
|
logger.info("%s is %s", LazyStr(self.get_class_name), LazyStr(Pingable.get_state_name, available))
|
|
345
345
|
return available
|
|
346
346
|
|
|
347
|
-
async def start(self, repeat: float, timeout: float):
|
|
347
|
+
async def start(self, repeat: float, timeout: float): # NOSONAR(S7483)
|
|
348
348
|
return await self._set_state(True, repeat, timeout)
|
|
349
349
|
|
|
350
|
-
async def stop(self, repeat: float, timeout: float):
|
|
350
|
+
async def stop(self, repeat: float, timeout: float): # NOSONAR(S7483)
|
|
351
351
|
return await self._set_state(False, repeat, timeout)
|
|
352
352
|
|
|
353
353
|
class Service(Manageable):
|
|
@@ -367,11 +367,11 @@ class Service(Manageable):
|
|
|
367
367
|
writer.close()
|
|
368
368
|
await writer.wait_closed()
|
|
369
369
|
|
|
370
|
-
async def _ping(self, availability_hint: bool | None = None):
|
|
370
|
+
async def _ping(self, availability_hint: bool | None = None): # NOSONAR(S1172)
|
|
371
371
|
logger.debug("Pinging %s (%s:%s)", LazyStr(self.get_class_name), str(self.host), str(self.port))
|
|
372
372
|
await self._connect(self.host, self.port)
|
|
373
373
|
|
|
374
|
-
async def ping(self, availability_hint: bool | None = None):
|
|
374
|
+
async def ping(self, availability_hint: bool | None = None): # NOSONAR(S3776)
|
|
375
375
|
try:
|
|
376
376
|
await self._ping(availability_hint)
|
|
377
377
|
return True
|
|
@@ -426,9 +426,9 @@ class SshService(Service):
|
|
|
426
426
|
asyncssh.connect(host, port, options=asyncssh.SSHClientConnectionOptions(
|
|
427
427
|
host_key_alias=self.host_name,
|
|
428
428
|
client_keys=_client_key(),
|
|
429
|
-
connect_timeout=self._connect_timeout))
|
|
429
|
+
connect_timeout=self._connect_timeout))
|
|
430
430
|
):
|
|
431
|
-
pass
|
|
431
|
+
pass # NOSONAR(S108)
|
|
432
432
|
|
|
433
433
|
class Device(Manageable):
|
|
434
434
|
def __init__(self, host: str, manager: Manager):
|
|
@@ -458,7 +458,7 @@ class StateSerializer:
|
|
|
458
458
|
@staticmethod
|
|
459
459
|
def loads(s: str):
|
|
460
460
|
try:
|
|
461
|
-
return
|
|
461
|
+
return {k: StateSerializer.load_value(v) for k, v in [s.split('=') for s in s.split(',')]}
|
|
462
462
|
except ValueError as e:
|
|
463
463
|
e.add_note("Missing '=' in state")
|
|
464
464
|
raise
|
|
@@ -468,7 +468,7 @@ class PhoneState(ABC):
|
|
|
468
468
|
PFTPD = 'pftpd'
|
|
469
469
|
|
|
470
470
|
@abstractmethod
|
|
471
|
-
async def get(self, repeat: float, timeout: float) -> dict:
|
|
471
|
+
async def get(self, repeat: float, timeout: float) -> dict: # NOSONAR(S7483)
|
|
472
472
|
pass
|
|
473
473
|
|
|
474
474
|
########
|
|
@@ -530,7 +530,7 @@ class ServiceResolver:
|
|
|
530
530
|
self.zeroconf = zeroconf
|
|
531
531
|
self.service_type = service_type
|
|
532
532
|
|
|
533
|
-
async def get(self, service_name: str, timeout: float = 3):
|
|
533
|
+
async def get(self, service_name: str, timeout: float = 3): # NOSONAR(S7483)
|
|
534
534
|
service_info = await self.zeroconf.async_get_service_info(self.service_type, f"{service_name}.{self.service_type}", timeout=int(timeout*1000))
|
|
535
535
|
if not service_info or not service_info.port:
|
|
536
536
|
raise TimeoutError("Unable to resolve zeroconf (DNS-SD) service information")
|
|
@@ -647,6 +647,7 @@ class PftpdServiceListener(ServiceListener):
|
|
|
647
647
|
logger.debug(" (ServiceListener) Resolved %s to %s:%d", service_name, host, port)
|
|
648
648
|
|
|
649
649
|
def del_service(self, service_name: str):
|
|
650
|
+
# we intentionally do not remove anything from the cache
|
|
650
651
|
pass
|
|
651
652
|
|
|
652
653
|
class RemotePftpd(SshService):
|
|
@@ -861,7 +862,7 @@ class Webhooks:
|
|
|
861
862
|
def __init__(self, host: str, port: int):
|
|
862
863
|
self.host = host
|
|
863
864
|
self.port = port
|
|
864
|
-
self.variables
|
|
865
|
+
self.variables: dict[str, asyncio.Queue[str]] = {}
|
|
865
866
|
|
|
866
867
|
@staticmethod
|
|
867
868
|
def get_ping_path():
|
|
@@ -872,7 +873,7 @@ class Webhooks:
|
|
|
872
873
|
return f'/{Webhooks.VARIABLE_PATH}/{variable}'
|
|
873
874
|
|
|
874
875
|
async def _start(self):
|
|
875
|
-
async def _ping(request: web.Request):
|
|
876
|
+
async def _ping(request: web.Request): # NOSONAR(S7503)
|
|
876
877
|
return web.Response(text='pong')
|
|
877
878
|
async def _receive_variable(request: web.Request):
|
|
878
879
|
queue = self.variables.get(request.match_info['name'])
|
|
@@ -899,7 +900,7 @@ class Webhooks:
|
|
|
899
900
|
def unsubscribe_variable(self, variable: str):
|
|
900
901
|
self.variables.pop(variable)
|
|
901
902
|
|
|
902
|
-
async def get_variable(self, variable: str, timeout: float):
|
|
903
|
+
async def get_variable(self, variable: str, timeout: float): # NOSONAR(S7483)
|
|
903
904
|
queue = self.variables.get(variable)
|
|
904
905
|
if not queue:
|
|
905
906
|
raise ValueError(f"The {variable} is unknown")
|
|
@@ -913,6 +914,7 @@ class Webhooks:
|
|
|
913
914
|
def __enter__(self):
|
|
914
915
|
raise TypeError("Use async with instead")
|
|
915
916
|
def __exit__(self, exc_type, exc_value, exc_tb):
|
|
917
|
+
# can't be called
|
|
916
918
|
pass
|
|
917
919
|
async def __aenter__(self):
|
|
918
920
|
await self._start()
|
|
@@ -936,7 +938,7 @@ class Automate:
|
|
|
936
938
|
"payload": f"prim-ctrl;{time.time()};" + message
|
|
937
939
|
}
|
|
938
940
|
logger.debug("Messaging Automate with: %s", message)
|
|
939
|
-
async with self.session.post(
|
|
941
|
+
async with self.session.post('https://llamalab.com/automate/cloud/message', json=data) as response:
|
|
940
942
|
await response.text()
|
|
941
943
|
|
|
942
944
|
class AutomatePftpdManager(Manager):
|
|
@@ -1072,9 +1074,9 @@ class WideHelpFormatter(argparse.RawTextHelpFormatter):
|
|
|
1072
1074
|
|
|
1073
1075
|
async def gather_with_taskgroup(*coros):
|
|
1074
1076
|
try:
|
|
1075
|
-
async with asyncio.TaskGroup() as tg:
|
|
1077
|
+
async with asyncio.TaskGroup() as tg: # NOSONAR(S7513)
|
|
1076
1078
|
tasks = [tg.create_task(coro) for coro in coros]
|
|
1077
|
-
return tuple(
|
|
1079
|
+
return tuple(task.result() for task in tasks)
|
|
1078
1080
|
except ExceptionGroup as eg:
|
|
1079
1081
|
exc = eg.exceptions[0]
|
|
1080
1082
|
# this can be captured in another TaskGroup that drops traceback information from "from e"
|
|
@@ -1111,6 +1113,7 @@ class Control:
|
|
|
1111
1113
|
|
|
1112
1114
|
@staticmethod
|
|
1113
1115
|
def setup_parser_options(parser):
|
|
1116
|
+
# no options
|
|
1114
1117
|
pass
|
|
1115
1118
|
|
|
1116
1119
|
@staticmethod
|
|
@@ -1145,12 +1148,12 @@ class Control:
|
|
|
1145
1148
|
self.phone = phone
|
|
1146
1149
|
self.keyboard_interrupt = keyboard_interrupt
|
|
1147
1150
|
|
|
1148
|
-
async def _stop(self, restore_state: dict | None, stop_only_started: bool = False):
|
|
1151
|
+
async def _stop(self, restore_state: dict | None, stop_only_started: bool = False): # NOSONAR(S3776)
|
|
1149
1152
|
with self.keyboard_interrupt.protect():
|
|
1150
1153
|
async def _suppress(coro, default: bool):
|
|
1151
1154
|
try:
|
|
1152
1155
|
return await coro
|
|
1153
|
-
except:
|
|
1156
|
+
except: # NOSONAR(S5754)
|
|
1154
1157
|
return default
|
|
1155
1158
|
if self.local.vpn and self.phone.vpn and self.phone.remote_sftp and await _suppress(self.local.vpn.test(), True) and await _suppress(self.phone.vpn.test(), True):
|
|
1156
1159
|
if (restore_state is None or not restore_state.get(Control.PHONE_SFTP, stop_only_started)) and await _suppress(self.phone.remote_sftp.test(), True):
|
|
@@ -1176,7 +1179,7 @@ class Control:
|
|
|
1176
1179
|
except Exception as e:
|
|
1177
1180
|
logger.exception_or_error(e)
|
|
1178
1181
|
|
|
1179
|
-
async def run(self):
|
|
1182
|
+
async def run(self): # NOSONAR(S3776)
|
|
1180
1183
|
match self.args.intent:
|
|
1181
1184
|
case 'test':
|
|
1182
1185
|
if self.local.vpn and self.phone.vpn and self.phone.remote_sftp and self.phone.state:
|
|
@@ -1188,7 +1191,7 @@ class Control:
|
|
|
1188
1191
|
await self.phone.zeroconf_sftp.test()
|
|
1189
1192
|
case 'start':
|
|
1190
1193
|
if self.local.vpn and self.phone.vpn and self.phone.remote_sftp:
|
|
1191
|
-
state =
|
|
1194
|
+
state = {}
|
|
1192
1195
|
try:
|
|
1193
1196
|
# gather local state info
|
|
1194
1197
|
local_vpn_state = await self.local.vpn.test()
|
|
@@ -1208,7 +1211,7 @@ class Control:
|
|
|
1208
1211
|
state[Control.PHONE_VPN] = phone_vpn_state
|
|
1209
1212
|
state[Control.PHONE_SFTP] = phone_state[PhoneState.PFTPD]
|
|
1210
1213
|
if not state[Control.PHONE_WIFI] and not self.args.accept_cellular:
|
|
1211
|
-
raise RuntimeError(
|
|
1214
|
+
raise RuntimeError("Phone is not on Wi-Fi network")
|
|
1212
1215
|
else:
|
|
1213
1216
|
state[Control.PHONE_VPN] = phone_vpn_state = await self.phone.vpn.test()
|
|
1214
1217
|
if phone_vpn_state:
|
|
@@ -1259,7 +1262,7 @@ class Control:
|
|
|
1259
1262
|
raise RuntimeError(f"Even when {self.phone.vpn.get_class_name()} and {self.phone.remote_sftp.get_class_name()} is started, {self.phone.remote_sftp.get_class_name()} is still not accessible")
|
|
1260
1263
|
# print out result on stdout
|
|
1261
1264
|
if not self.args.backup_state:
|
|
1262
|
-
state =
|
|
1265
|
+
state = {}
|
|
1263
1266
|
state[Control.CONNECTED] = Control.ZEROCONF if zeroconf_accessible else Control.REMOTE
|
|
1264
1267
|
print(StateSerializer.dumps(state))
|
|
1265
1268
|
except:
|
|
@@ -3,7 +3,7 @@ packages = [{include = "prim_ctrl"}]
|
|
|
3
3
|
|
|
4
4
|
[project]
|
|
5
5
|
name = "prim-ctrl"
|
|
6
|
-
version = "0.8.
|
|
6
|
+
version = "0.8.2"
|
|
7
7
|
description = "Primitive Ctrl - Remote control of your phone's Primitive FTPd Android SFTP server and optionally Tailscale VPN."
|
|
8
8
|
license = "Apache-2.0"
|
|
9
9
|
authors = [
|
|
File without changes
|
|
File without changes
|
|
File without changes
|