prim-ctrl 0.8.0__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.0 → prim_ctrl-0.8.2}/PKG-INFO +1 -1
- {prim_ctrl-0.8.0 → prim_ctrl-0.8.2}/prim_ctrl/__main__.py +68 -56
- {prim_ctrl-0.8.0 → prim_ctrl-0.8.2}/pyproject.toml +1 -1
- {prim_ctrl-0.8.0 → prim_ctrl-0.8.2}/LICENSE +0 -0
- {prim_ctrl-0.8.0 → prim_ctrl-0.8.2}/README.md +0 -0
- {prim_ctrl-0.8.0 → 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()
|
|
@@ -225,12 +225,10 @@ class Subprocess:
|
|
|
225
225
|
# Note: aiohttp's AsyncResolver can't be used when asyncio.create_subprocess_exec is used
|
|
226
226
|
# aiohttp's AsyncResolver uses aiodns, that needs a SelectorEventLoop on Windows, and that's loop.subprocess_exec is not implemented, but required by asyncio.create_subprocess_exec
|
|
227
227
|
class ExternalDnsResolver(DnsResolver):
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
def __init__(self, where: str = EXTERNAL_DNS):
|
|
228
|
+
def __init__(self, where: str):
|
|
231
229
|
self.where = where
|
|
232
230
|
self.dns_resolver = None
|
|
233
|
-
self.cache
|
|
231
|
+
self.cache: dict[tuple[str, int, socket.AddressFamily], tuple[float, list[ResolveResult]]] = {}
|
|
234
232
|
|
|
235
233
|
async def resolve(self, host: str, port: int = 0, family: socket.AddressFamily = socket.AF_UNSPEC) -> list[ResolveResult]:
|
|
236
234
|
logger.debug("Resolving DNS at %s for %s:%i (%s)", self.where, host, port, "ipv6" if family == socket.AF_INET6 else "ipv4")
|
|
@@ -250,8 +248,8 @@ class ExternalDnsResolver(DnsResolver):
|
|
|
250
248
|
self.dns_resolver = await dns.asyncresolver.make_resolver_at(self.where)
|
|
251
249
|
answer = await self.dns_resolver.resolve(host, rdtype=dns.rdatatype.AAAA if family == socket.AF_INET6 else dns.rdatatype.A)
|
|
252
250
|
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer) as e:
|
|
253
|
-
msg = e.args
|
|
254
|
-
exc =
|
|
251
|
+
msg = '; '.join(e.args) if len(e.args) else "DNS lookup failed"
|
|
252
|
+
exc = LookupError(msg)
|
|
255
253
|
# this is captured in a TaskGroup that drops traceback information from "from e"
|
|
256
254
|
exc.add_note(repr(e))
|
|
257
255
|
raise exc from None
|
|
@@ -271,7 +269,7 @@ class ExternalDnsResolver(DnsResolver):
|
|
|
271
269
|
)
|
|
272
270
|
)
|
|
273
271
|
if not hosts:
|
|
274
|
-
raise
|
|
272
|
+
raise LookupError("DNS lookup failed")
|
|
275
273
|
|
|
276
274
|
self.cache[key] = (answer.expiration, hosts)
|
|
277
275
|
return hosts
|
|
@@ -293,7 +291,7 @@ class Pingable(ABC):
|
|
|
293
291
|
def get_state_name(available: bool):
|
|
294
292
|
return 'up' if available else 'down'
|
|
295
293
|
|
|
296
|
-
async def wait_for(self, available: bool, timeout: float):
|
|
294
|
+
async def wait_for(self, available: bool, timeout: float): # NOSONAR(S7483)
|
|
297
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))
|
|
298
296
|
async with asyncio.timeout(timeout):
|
|
299
297
|
while await self.ping(available) != available:
|
|
@@ -317,7 +315,7 @@ class Manageable(Pingable):
|
|
|
317
315
|
super().__init__()
|
|
318
316
|
self.manager = manager
|
|
319
317
|
|
|
320
|
-
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)
|
|
321
319
|
action_name = LazyStr(lambda: 'Starting' if available else 'Stopping')
|
|
322
320
|
class_name = LazyStr(self.get_class_name)
|
|
323
321
|
available_name = LazyStr(Pingable.get_state_name, available)
|
|
@@ -346,10 +344,10 @@ class Manageable(Pingable):
|
|
|
346
344
|
logger.info("%s is %s", LazyStr(self.get_class_name), LazyStr(Pingable.get_state_name, available))
|
|
347
345
|
return available
|
|
348
346
|
|
|
349
|
-
async def start(self, repeat: float, timeout: float):
|
|
347
|
+
async def start(self, repeat: float, timeout: float): # NOSONAR(S7483)
|
|
350
348
|
return await self._set_state(True, repeat, timeout)
|
|
351
349
|
|
|
352
|
-
async def stop(self, repeat: float, timeout: float):
|
|
350
|
+
async def stop(self, repeat: float, timeout: float): # NOSONAR(S7483)
|
|
353
351
|
return await self._set_state(False, repeat, timeout)
|
|
354
352
|
|
|
355
353
|
class Service(Manageable):
|
|
@@ -369,11 +367,11 @@ class Service(Manageable):
|
|
|
369
367
|
writer.close()
|
|
370
368
|
await writer.wait_closed()
|
|
371
369
|
|
|
372
|
-
async def _ping(self, availability_hint: bool | None = None):
|
|
370
|
+
async def _ping(self, availability_hint: bool | None = None): # NOSONAR(S1172)
|
|
373
371
|
logger.debug("Pinging %s (%s:%s)", LazyStr(self.get_class_name), str(self.host), str(self.port))
|
|
374
372
|
await self._connect(self.host, self.port)
|
|
375
373
|
|
|
376
|
-
async def ping(self, availability_hint: bool | None = None):
|
|
374
|
+
async def ping(self, availability_hint: bool | None = None): # NOSONAR(S3776)
|
|
377
375
|
try:
|
|
378
376
|
await self._ping(availability_hint)
|
|
379
377
|
return True
|
|
@@ -428,9 +426,9 @@ class SshService(Service):
|
|
|
428
426
|
asyncssh.connect(host, port, options=asyncssh.SSHClientConnectionOptions(
|
|
429
427
|
host_key_alias=self.host_name,
|
|
430
428
|
client_keys=_client_key(),
|
|
431
|
-
connect_timeout=self._connect_timeout))
|
|
429
|
+
connect_timeout=self._connect_timeout))
|
|
432
430
|
):
|
|
433
|
-
pass
|
|
431
|
+
pass # NOSONAR(S108)
|
|
434
432
|
|
|
435
433
|
class Device(Manageable):
|
|
436
434
|
def __init__(self, host: str, manager: Manager):
|
|
@@ -460,7 +458,7 @@ class StateSerializer:
|
|
|
460
458
|
@staticmethod
|
|
461
459
|
def loads(s: str):
|
|
462
460
|
try:
|
|
463
|
-
return
|
|
461
|
+
return {k: StateSerializer.load_value(v) for k, v in [s.split('=') for s in s.split(',')]}
|
|
464
462
|
except ValueError as e:
|
|
465
463
|
e.add_note("Missing '=' in state")
|
|
466
464
|
raise
|
|
@@ -470,7 +468,7 @@ class PhoneState(ABC):
|
|
|
470
468
|
PFTPD = 'pftpd'
|
|
471
469
|
|
|
472
470
|
@abstractmethod
|
|
473
|
-
async def get(self, repeat: float, timeout: float) -> dict:
|
|
471
|
+
async def get(self, repeat: float, timeout: float) -> dict: # NOSONAR(S7483)
|
|
474
472
|
pass
|
|
475
473
|
|
|
476
474
|
########
|
|
@@ -532,7 +530,7 @@ class ServiceResolver:
|
|
|
532
530
|
self.zeroconf = zeroconf
|
|
533
531
|
self.service_type = service_type
|
|
534
532
|
|
|
535
|
-
async def get(self, service_name: str, timeout: float = 3):
|
|
533
|
+
async def get(self, service_name: str, timeout: float = 3): # NOSONAR(S7483)
|
|
536
534
|
service_info = await self.zeroconf.async_get_service_info(self.service_type, f"{service_name}.{self.service_type}", timeout=int(timeout*1000))
|
|
537
535
|
if not service_info or not service_info.port:
|
|
538
536
|
raise TimeoutError("Unable to resolve zeroconf (DNS-SD) service information")
|
|
@@ -649,6 +647,7 @@ class PftpdServiceListener(ServiceListener):
|
|
|
649
647
|
logger.debug(" (ServiceListener) Resolved %s to %s:%d", service_name, host, port)
|
|
650
648
|
|
|
651
649
|
def del_service(self, service_name: str):
|
|
650
|
+
# we intentionally do not remove anything from the cache
|
|
652
651
|
pass
|
|
653
652
|
|
|
654
653
|
class RemotePftpd(SshService):
|
|
@@ -704,14 +703,14 @@ class Tailscale():
|
|
|
704
703
|
class Funnel(Pingable):
|
|
705
704
|
LOCAL_HOST = '127.0.0.1'
|
|
706
705
|
|
|
707
|
-
def __init__(self, tailscale: Tailscale, machine_name: str, local_port: int, local_path: str, external_port: int,
|
|
706
|
+
def __init__(self, tailscale: Tailscale, machine_name: str, local_port: int, local_path: str, external_port: int, local_tailscale: LocalTailscale):
|
|
708
707
|
self.machine_name = machine_name
|
|
709
708
|
self.local_port = local_port
|
|
710
709
|
self.external_name = f'{machine_name}.{tailscale.tailnet}'
|
|
711
710
|
self.external_port = external_port
|
|
712
711
|
self.external_url = f'https://{machine_name}.{tailscale.tailnet}:{external_port}{local_path}'
|
|
713
|
-
self.external_public_dns_resolver = dns_resolver
|
|
714
712
|
self.local_tailscale = local_tailscale
|
|
713
|
+
self.external_public_dns_resolver = None
|
|
715
714
|
self.external_tailscale_dns_resolver = None
|
|
716
715
|
|
|
717
716
|
async def wait_for(self, available: bool, timeout: float):
|
|
@@ -720,15 +719,17 @@ class Funnel(Pingable):
|
|
|
720
719
|
|
|
721
720
|
async def ping(self, availability_hint: bool | None = None):
|
|
722
721
|
logger.debug("Resolving DNS for %s (%s:%s)", LazyStr(self.get_class_name), self.external_name, self.external_port)
|
|
723
|
-
if self.external_tailscale_dns_resolver is None:
|
|
724
|
-
self.external_tailscale_dns_resolver = ExternalDnsResolver(await self.local_tailscale.external_dns_resolver())
|
|
725
722
|
# first try at Tailscale's DNS, if it doesn't know, we should not resolve at a public DNS and cache nxdomain for 5 minutes
|
|
723
|
+
if self.external_tailscale_dns_resolver is None:
|
|
724
|
+
self.external_tailscale_dns_resolver = ExternalDnsResolver(await self.local_tailscale.external_tailscale_dns_resolver())
|
|
726
725
|
try:
|
|
727
726
|
_answer = await self.external_tailscale_dns_resolver.resolve(self.external_name, self.external_port)
|
|
728
727
|
except Exception as e:
|
|
729
728
|
logger.debug("Resolving at Tailscale's external DNS has failed: %s", LazyStr(repr, e))
|
|
730
729
|
return False
|
|
731
730
|
# then try at a public DNS
|
|
731
|
+
if self.external_public_dns_resolver is None:
|
|
732
|
+
self.external_public_dns_resolver = ExternalDnsResolver(await self.local_tailscale.external_public_dns_resolver())
|
|
732
733
|
try:
|
|
733
734
|
_answer = await self.external_public_dns_resolver.resolve(self.external_name, self.external_port)
|
|
734
735
|
except Exception as e:
|
|
@@ -791,7 +792,7 @@ class LocalTailscale(Manageable):
|
|
|
791
792
|
self._is_started_now = start_result
|
|
792
793
|
if start_result and not self._checked_fresh_start and self.machine_name:
|
|
793
794
|
self._checked_fresh_start = True
|
|
794
|
-
max_last_seen_age =
|
|
795
|
+
max_last_seen_age = 7200
|
|
795
796
|
wait_on_fresh_start = 5
|
|
796
797
|
difference = datetime.now(timezone.utc).replace(microsecond=0) - device_info.last_seen if device_info.last_seen else None
|
|
797
798
|
difference_sec = difference.total_seconds() if difference else None
|
|
@@ -803,16 +804,23 @@ class LocalTailscale(Manageable):
|
|
|
803
804
|
await asyncio.sleep(wait_on_fresh_start)
|
|
804
805
|
return start_result
|
|
805
806
|
|
|
806
|
-
async def
|
|
807
|
-
logger.debug("Getting external DNS resolver of %s for ts.net", LazyStr(self.get_class_name))
|
|
807
|
+
async def _tailscale_dns_status(self):
|
|
808
808
|
success, stdout, stderr = await Subprocess.tailscale(['dns', 'status', '--json'])
|
|
809
809
|
if not success:
|
|
810
810
|
exc = RuntimeError("Failed to get local DNS status from local Tailscale")
|
|
811
811
|
exc.add_note(stderr.rstrip().replace("\n", "; "))
|
|
812
812
|
raise exc
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
813
|
+
return json.loads(stdout)
|
|
814
|
+
|
|
815
|
+
async def external_public_dns_resolver(self):
|
|
816
|
+
logger.debug("Getting external public DNS resolver of %s", LazyStr(self.get_class_name))
|
|
817
|
+
status = await self._tailscale_dns_status()
|
|
818
|
+
return str(status['SystemDNS']['Nameservers'][0])
|
|
819
|
+
|
|
820
|
+
async def external_tailscale_dns_resolver(self):
|
|
821
|
+
logger.debug("Getting external Tailscale DNS resolver of %s for ts.net", LazyStr(self.get_class_name))
|
|
822
|
+
status = await self._tailscale_dns_status()
|
|
823
|
+
return str(next(addr['Addr'] for addr in status['SplitDNSRoutes']['ts.net.'] if ':' not in addr['Addr']))
|
|
816
824
|
|
|
817
825
|
class StatSeen(ABC):
|
|
818
826
|
@abstractmethod
|
|
@@ -854,7 +862,7 @@ class Webhooks:
|
|
|
854
862
|
def __init__(self, host: str, port: int):
|
|
855
863
|
self.host = host
|
|
856
864
|
self.port = port
|
|
857
|
-
self.variables
|
|
865
|
+
self.variables: dict[str, asyncio.Queue[str]] = {}
|
|
858
866
|
|
|
859
867
|
@staticmethod
|
|
860
868
|
def get_ping_path():
|
|
@@ -865,7 +873,7 @@ class Webhooks:
|
|
|
865
873
|
return f'/{Webhooks.VARIABLE_PATH}/{variable}'
|
|
866
874
|
|
|
867
875
|
async def _start(self):
|
|
868
|
-
async def _ping(request: web.Request):
|
|
876
|
+
async def _ping(request: web.Request): # NOSONAR(S7503)
|
|
869
877
|
return web.Response(text='pong')
|
|
870
878
|
async def _receive_variable(request: web.Request):
|
|
871
879
|
queue = self.variables.get(request.match_info['name'])
|
|
@@ -892,7 +900,7 @@ class Webhooks:
|
|
|
892
900
|
def unsubscribe_variable(self, variable: str):
|
|
893
901
|
self.variables.pop(variable)
|
|
894
902
|
|
|
895
|
-
async def get_variable(self, variable: str, timeout: float):
|
|
903
|
+
async def get_variable(self, variable: str, timeout: float): # NOSONAR(S7483)
|
|
896
904
|
queue = self.variables.get(variable)
|
|
897
905
|
if not queue:
|
|
898
906
|
raise ValueError(f"The {variable} is unknown")
|
|
@@ -906,6 +914,7 @@ class Webhooks:
|
|
|
906
914
|
def __enter__(self):
|
|
907
915
|
raise TypeError("Use async with instead")
|
|
908
916
|
def __exit__(self, exc_type, exc_value, exc_tb):
|
|
917
|
+
# can't be called
|
|
909
918
|
pass
|
|
910
919
|
async def __aenter__(self):
|
|
911
920
|
await self._start()
|
|
@@ -929,7 +938,7 @@ class Automate:
|
|
|
929
938
|
"payload": f"prim-ctrl;{time.time()};" + message
|
|
930
939
|
}
|
|
931
940
|
logger.debug("Messaging Automate with: %s", message)
|
|
932
|
-
async with self.session.post(
|
|
941
|
+
async with self.session.post('https://llamalab.com/automate/cloud/message', json=data) as response:
|
|
933
942
|
await response.text()
|
|
934
943
|
|
|
935
944
|
class AutomatePftpdManager(Manager):
|
|
@@ -991,12 +1000,12 @@ class ExternalWebhookPing(WebhookPing):
|
|
|
991
1000
|
class AutomatePhoneState(PhoneState):
|
|
992
1001
|
VARIABLE_STATE = 'state'
|
|
993
1002
|
|
|
994
|
-
def __init__(self, general_session: aiohttp.ClientSession,
|
|
1003
|
+
def __init__(self, general_session: aiohttp.ClientSession, external_public_dns_session: aiohttp.ClientSession, webhooks: Webhooks, automate: Automate, funnel: Funnel, local_tailscale: LocalTailscale):
|
|
995
1004
|
self.webhooks = webhooks
|
|
996
1005
|
self.automate = automate
|
|
997
1006
|
self.funnel = funnel
|
|
998
1007
|
self.local_webhook_ping = WebhookPing(general_session, funnel)
|
|
999
|
-
self.external_webhook_ping = ExternalWebhookPing(
|
|
1008
|
+
self.external_webhook_ping = ExternalWebhookPing(external_public_dns_session, funnel, local_tailscale)
|
|
1000
1009
|
|
|
1001
1010
|
async def get(self, repeat: float, timeout: float):
|
|
1002
1011
|
logger.info("Getting Phone state...")
|
|
@@ -1065,9 +1074,9 @@ class WideHelpFormatter(argparse.RawTextHelpFormatter):
|
|
|
1065
1074
|
|
|
1066
1075
|
async def gather_with_taskgroup(*coros):
|
|
1067
1076
|
try:
|
|
1068
|
-
async with asyncio.TaskGroup() as tg:
|
|
1077
|
+
async with asyncio.TaskGroup() as tg: # NOSONAR(S7513)
|
|
1069
1078
|
tasks = [tg.create_task(coro) for coro in coros]
|
|
1070
|
-
return tuple(
|
|
1079
|
+
return tuple(task.result() for task in tasks)
|
|
1071
1080
|
except ExceptionGroup as eg:
|
|
1072
1081
|
exc = eg.exceptions[0]
|
|
1073
1082
|
# this can be captured in another TaskGroup that drops traceback information from "from e"
|
|
@@ -1104,6 +1113,7 @@ class Control:
|
|
|
1104
1113
|
|
|
1105
1114
|
@staticmethod
|
|
1106
1115
|
def setup_parser_options(parser):
|
|
1116
|
+
# no options
|
|
1107
1117
|
pass
|
|
1108
1118
|
|
|
1109
1119
|
@staticmethod
|
|
@@ -1138,12 +1148,12 @@ class Control:
|
|
|
1138
1148
|
self.phone = phone
|
|
1139
1149
|
self.keyboard_interrupt = keyboard_interrupt
|
|
1140
1150
|
|
|
1141
|
-
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)
|
|
1142
1152
|
with self.keyboard_interrupt.protect():
|
|
1143
1153
|
async def _suppress(coro, default: bool):
|
|
1144
1154
|
try:
|
|
1145
1155
|
return await coro
|
|
1146
|
-
except:
|
|
1156
|
+
except: # NOSONAR(S5754)
|
|
1147
1157
|
return default
|
|
1148
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):
|
|
1149
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):
|
|
@@ -1169,7 +1179,7 @@ class Control:
|
|
|
1169
1179
|
except Exception as e:
|
|
1170
1180
|
logger.exception_or_error(e)
|
|
1171
1181
|
|
|
1172
|
-
async def run(self):
|
|
1182
|
+
async def run(self): # NOSONAR(S3776)
|
|
1173
1183
|
match self.args.intent:
|
|
1174
1184
|
case 'test':
|
|
1175
1185
|
if self.local.vpn and self.phone.vpn and self.phone.remote_sftp and self.phone.state:
|
|
@@ -1181,7 +1191,7 @@ class Control:
|
|
|
1181
1191
|
await self.phone.zeroconf_sftp.test()
|
|
1182
1192
|
case 'start':
|
|
1183
1193
|
if self.local.vpn and self.phone.vpn and self.phone.remote_sftp:
|
|
1184
|
-
state =
|
|
1194
|
+
state = {}
|
|
1185
1195
|
try:
|
|
1186
1196
|
# gather local state info
|
|
1187
1197
|
local_vpn_state = await self.local.vpn.test()
|
|
@@ -1201,7 +1211,7 @@ class Control:
|
|
|
1201
1211
|
state[Control.PHONE_VPN] = phone_vpn_state
|
|
1202
1212
|
state[Control.PHONE_SFTP] = phone_state[PhoneState.PFTPD]
|
|
1203
1213
|
if not state[Control.PHONE_WIFI] and not self.args.accept_cellular:
|
|
1204
|
-
raise RuntimeError(
|
|
1214
|
+
raise RuntimeError("Phone is not on Wi-Fi network")
|
|
1205
1215
|
else:
|
|
1206
1216
|
state[Control.PHONE_VPN] = phone_vpn_state = await self.phone.vpn.test()
|
|
1207
1217
|
if phone_vpn_state:
|
|
@@ -1252,7 +1262,7 @@ class Control:
|
|
|
1252
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")
|
|
1253
1263
|
# print out result on stdout
|
|
1254
1264
|
if not self.args.backup_state:
|
|
1255
|
-
state =
|
|
1265
|
+
state = {}
|
|
1256
1266
|
state[Control.CONNECTED] = Control.ZEROCONF if zeroconf_accessible else Control.REMOTE
|
|
1257
1267
|
print(StateSerializer.dumps(state))
|
|
1258
1268
|
except:
|
|
@@ -1342,15 +1352,11 @@ class AutomateControl(Control):
|
|
|
1342
1352
|
async def runner(args: argparse.Namespace) -> None:
|
|
1343
1353
|
AutomateControl.prepare(args)
|
|
1344
1354
|
|
|
1345
|
-
external_dns_resolver = ExternalDnsResolver()
|
|
1346
1355
|
async with (
|
|
1347
1356
|
aiohttp.ClientSession() as general_session,
|
|
1348
1357
|
aiohttp.ClientSession(
|
|
1349
1358
|
# Automate messaging server prefers closing connections
|
|
1350
1359
|
connector=aiohttp.TCPConnector(force_close=True)) as force_close_session,
|
|
1351
|
-
aiohttp.ClientSession(
|
|
1352
|
-
# Uses external DNS to access Funnel TCP forwarder servers instead of local MagicDNS route
|
|
1353
|
-
connector=aiohttp.TCPConnector(resolver=external_dns_resolver)) as external_dns_session,
|
|
1354
1360
|
AsyncZeroconf() as zeroconf
|
|
1355
1361
|
):
|
|
1356
1362
|
service_cache = ServiceCache(Cache(Cache.PRIM_SYNC_APP_NAME))
|
|
@@ -1385,14 +1391,20 @@ class AutomateControl(Control):
|
|
|
1385
1391
|
tailscale = Tailscale(secrets, general_session, _tailscale_tailnet(), _tailscale_secretfile()) if args.tailscale else None
|
|
1386
1392
|
local_tailscale = LocalTailscale(tailscale, _funnel_local_machine_name_or_none(), LocalTailscaleManager()) if tailscale else None
|
|
1387
1393
|
remote_tailscale = RemoteTailscale(tailscale, _tailscale_remote_machine_name(), AutomateTailscaleManager(automate)) if tailscale else None
|
|
1388
|
-
funnel = Funnel(tailscale, _funnel_local_machine_name(), _funnel_local_port(), _funnel_local_path(), _funnel_external_port(),
|
|
1394
|
+
funnel = Funnel(tailscale, _funnel_local_machine_name(), _funnel_local_port(), _funnel_local_path(), _funnel_external_port(), local_tailscale) if tailscale and args.funnel and local_tailscale else None
|
|
1389
1395
|
pftpd_manager = AutomatePftpdManager(automate)
|
|
1390
1396
|
zeroconf_pftpd = ZeroconfPftpd(args.server_name, service_cache, service_resolver, args.keyfile, pftpd_manager)
|
|
1391
1397
|
remote_pftpd = RemotePftpd(remote_tailscale.host, _tailscale_sftp_port(), args.server_name, args.keyfile, pftpd_manager) if remote_tailscale else None
|
|
1392
1398
|
|
|
1393
|
-
async with
|
|
1399
|
+
async with (
|
|
1400
|
+
aiohttp.ClientSession(
|
|
1401
|
+
# Uses external DNS to access Funnel TCP forwarder servers instead of local MagicDNS route
|
|
1402
|
+
connector=aiohttp.TCPConnector(resolver=funnel.external_public_dns_resolver)
|
|
1403
|
+
) if funnel else nullcontext() as external_public_dns_session,
|
|
1404
|
+
Webhooks(Funnel.LOCAL_HOST, funnel.local_port) if funnel else nullcontext() as webhooks
|
|
1405
|
+
):
|
|
1394
1406
|
local = Local(local_tailscale)
|
|
1395
|
-
automate_phone_state = AutomatePhoneState(general_session,
|
|
1407
|
+
automate_phone_state = AutomatePhoneState(general_session, external_public_dns_session, webhooks, automate, funnel, local_tailscale) if external_public_dns_session and webhooks and funnel and local_tailscale else None
|
|
1396
1408
|
phone = Phone(zeroconf_pftpd, remote_tailscale, remote_pftpd, automate_phone_state)
|
|
1397
1409
|
keyboard_interrupt = SignalFence(signal.SIGINT, lambda signum, frame: logger.debug("Keyboard interrupt received, finishing ongoing operations before exit"))
|
|
1398
1410
|
control = AutomateControl(args, local, phone, keyboard_interrupt)
|
|
@@ -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
|