This commit is contained in:
Tony Garnock-Jones 2022-02-09 15:50:10 +01:00
parent 3c91e5d74b
commit b8ba97742d
11 changed files with 296 additions and 37 deletions

View File

@ -0,0 +1,19 @@
; Sending <run-oneshot SOMECOMMAND RESTARTPOLICY> causes the command to be run.
;
?? <run-oneshot ?argv ?restartPolicy> [
let ?id = timestamp
let ?facet = facet
let ?d = <temporary-exec $id $argv>
<run-service <daemon $d>>
<daemon $d {
argv: $argv,
readyOnStart: #f,
restart: $restartPolicy,
}>
? <service-state <daemon $d> complete> [$facet ! stop]
? <service-state <daemon $d> failed> [$facet ! stop]
]
; If the restart policy is not specified, it is defaulted to `on-error`.
;
?? <run-oneshot ?argv> ! <run-oneshot $argv on-error>

View File

@ -1,2 +1,7 @@
let ?ds = dataspace
<machine-dataspace $ds>
$ds ? ?r [
$log ! <log "-" { line: "machine" |+++|: $r }>
?- $log ! <log "-" { line: "machine" |---|: $r }>
]

View File

@ -20,30 +20,6 @@
<daemon <udhcpc $ifname> ["udhcpc" "-i" $ifname "-fR" "-s" "/usr/lib/synit/udhcpc.script"]>
]
? <run-service <daemon <wpa_supplicant ?ifname>>> [
<daemon <wpa_supplicant $ifname> [
"wpa_supplicant" "-Dnl80211,wext" "-C/run/wpa_supplicant" "-i" $ifname
]>
]
?? <run-oneshot ?argv> ! <run-oneshot $argv on-error>
?? <run-oneshot ?argv ?restartPolicy> [
let ?id = timestamp
let ?facet = facet
let ?d = <temporary-exec $id $argv>
<run-service <daemon $d>>
<daemon $d {
argv: $argv,
readyOnStart: #f,
restart: $restartPolicy,
}>
? <service-state <daemon $d> complete> [$facet ! stop]
? <service-state <daemon $d> failed> [$facet ! stop]
]
;---------------------------------------------------------------------------
<daemon interface-monitor {
argv: "/usr/lib/synit/interface-monitor"
protocol: application/syndicate
@ -56,22 +32,10 @@
}
]
$machine ? ?r [
$log ! <log "-" { line: "machine" |+++|: $r }>
?- $log ! <log "-" { line: "machine" |---|: $r }>
]
$machine ? <interface ?ifname _ normal _ _ carrier _> [
$config <configure-interface $ifname <dhcp>>
]
$machine ? <interface ?ifname _ wireless _ _ _ _> [
$config <run-service <daemon <wpa_supplicant $ifname>>>
]
$machine ? <interface ?ifname _ wireless up up carrier _> [
$config <configure-interface $ifname <dhcp>>
]
$machine ? <route ?af default _ _ _ _> [
$config <default-route $af>
]

View File

@ -0,0 +1,20 @@
let ?settingsDir = "/etc/syndicate/user-settings"
let ?settings = <* $config [
<rewrite ?item <user-setting $item>>
]>
<require-service <config-watcher $settingsDir { config: $settings }>>
<require-service <daemon user-settings-daemon>>
<daemon user-settings-daemon {
argv: "/usr/lib/synit/user-settings-daemon"
protocol: application/syndicate
}>
? <service-object <daemon user-settings-daemon> ?cap> [
$cap {
config: $config
settingsDir: $settingsDir
}
]

View File

@ -0,0 +1,34 @@
? <machine-dataspace ?machine> [
$machine ? <interface ?ifname _ wireless _ _ _ _> [
$config [
<require-service <daemon <wpa_supplicant $ifname>>>
<depends-on <daemon <wifi-daemon $ifname>> <service-state <daemon <wpa_supplicant $ifname>> up>>
<require-service <daemon <wifi-daemon $ifname>>>
]
]
$machine ? <interface ?ifname _ wireless up up carrier _> [
$config <configure-interface $ifname <dhcp>>
]
]
? <run-service <daemon <wifi-daemon ?ifname>>> [
<daemon <wifi-daemon $ifname> {
argv: "/usr/lib/synit/wifi-daemon"
protocol: application/syndicate
}>
? <machine-dataspace ?machine> [
? <service-object <daemon <wifi-daemon $ifname>> ?cap> [
$cap {
machine: $machine
ifname: $ifname
}
]
]
]
? <run-service <daemon <wpa_supplicant ?ifname>>> [
<daemon <wpa_supplicant $ifname> [
"wpa_supplicant" "-Dnl80211,wext" "-C/run/wpa_supplicant" "-i" $ifname
]>
]

View File

@ -20,6 +20,7 @@ mount -t cgroup2 none /sys/fs/cgroup
mount -o rw,remount /
mkdir -p /etc/syndicate/user-settings
mkdir -p /run/etc/syndicate/core
mkdir -p /usr/local/etc/syndicate/core
mkdir -p /run/etc/syndicate/services

View File

@ -12,7 +12,10 @@ import threading
import pyroute2
from pr2modules.iwutil import IW
schemas = preserves.schema.load_schema_file('/usr/share/synit/schemas/schema-bundle.prb')
try:
schemas = preserves.schema.load_schema_file('/usr/share/synit/schemas/schema-bundle.prb')
except:
schemas = preserves.schema.load_schema_file('/home/tonyg/src/syndicate-system/protocols/schema-bundle.bin')
network = schemas.network
class LenientFormatter(Formatter):

View File

@ -0,0 +1,48 @@
import sys
import os
import preserves.schema
import hashlib
from syndicate import patterns as P, relay, turn, dataspace, Symbol, canonicalize, stringify
from syndicate.actor import find_loop
from syndicate.during import During
try:
schemas = preserves.schema.load_schema_file('/usr/share/synit/schemas/schema-bundle.prb')
except:
schemas = preserves.schema.load_schema_file('/home/tonyg/src/syndicate-system/protocols/schema-bundle.bin')
userSettings = schemas.userSettings
def digest(item):
return hashlib.sha1(canonicalize(item)).hexdigest()
@relay.service(name='user_settings_daemon', debug=False)
@During().add_handler
def main(args):
config = args[Symbol('config')].embeddedValue
settingsDir = args[Symbol('settingsDir')]
def assert_item(item):
filename = os.path.join(settingsDir, digest(item) + '.pr')
turn.log.info(f'Asserting: {item} --> {filename}')
with open(filename, 'wt') as f:
f.write(stringify(item, indent=2) + '\n')
def retract_item(item):
filename = os.path.join(settingsDir, digest(item) + '.pr')
turn.log.info(f'Retracting: {item} --> {filename}')
try:
os.unlink(filename)
except FileNotFoundError:
pass
@dataspace.during(config, P.bind(P.quote(userSettings.Command(P.u_, P.u_))))
def handle_command(c):
c = userSettings.Command.try_decode(c)
if c is None: return
c.action._accept({
'assert': assert_item,
'retract': retract_item,
})
if c.reply.VARIANT.name == 'replyWanted':
turn.publish(c.reply.value, userSettings.CommandReply())

View File

@ -0,0 +1,155 @@
import sys
import os
import socket
import threading
import atexit
import preserves.schema
import time
import re
from syndicate import patterns as P, relay, turn, dataspace, Symbol
from syndicate.actor import find_loop
from syndicate.during import During
try:
schemas = preserves.schema.load_schema_file('/usr/share/synit/schemas/schema-bundle.prb')
except:
schemas = preserves.schema.load_schema_file('/home/tonyg/src/syndicate-system/protocols/schema-bundle.bin')
network = schemas.network
server_socket_dir = '/run/wpa_supplicant'
class WPASupplicantClient:
# https://w1.fi/wpa_supplicant/devel/ctrl_iface_page.html
#
# According to the code (!) any response starting with '<' is an event, not a reply.
def __init__(self, interface, log):
self.interface = interface
self.log = log
self.server_socket_path = os.path.join(server_socket_dir, interface)
self.client_socket_path = f'/tmp/wpa_cli_py_{interface}_{os.getpid()}'
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
self.sock.bind(self.client_socket_path)
self.sock.connect(self.server_socket_path)
self.send('ATTACH')
answer = self.recv(timeout_sec=3.0)
if answer != 'OK':
raise AssertionError(f'wpa_supplicant replied {repr(answer)} to ATTACH')
self.cleanup = lambda: self.close()
atexit.register(self.cleanup)
def close(self):
if self.cleanup:
atexit.unregister(self.cleanup)
self.cleanup = None
self.send('DETACH')
self.sock.close()
os.unlink(self.client_socket_path)
def send(self, command):
self.sock.send(command.encode('utf-8'))
def recv(self, timeout_sec=None):
self.sock.settimeout(timeout_sec)
try:
reply = self.sock.recv(65536)
self.log.info(f'Raw input: {repr(reply)}')
return reply.strip().decode('utf-8')
except TimeoutError:
return None
finally:
self.sock.settimeout(None)
def gather_events(facet, callback, client, loop):
facet.log.debug('Background socket read thread started')
try:
while True:
facet.log.debug('waiting for event...')
e = client.recv()
# OMG python's incredibly broken mutable-variable-closures bit me AGAIN. I've had
# to add a layer of gratuitous rebinding of `e` plus the `lambda:` below to make it
# properly close over the current binding of `e`.
def handler_for_specific_e(e):
return lambda: callback(e)
turn.external(facet, handler_for_specific_e(e), loop=loop)
except Exception as e:
facet.log.debug(e)
finally:
facet.log.debug('Background socket read thread terminated')
@relay.service(name='wifi_daemon', debug=False)
@During().add_handler
def main(args):
machine_ds = args[Symbol('machine')].embeddedValue
ifname = args[Symbol('ifname')]
client = WPASupplicantClient(ifname, turn.log)
commands = []
active_network_id = None
def send_next_command():
while len(commands) > 0:
turn.log.info(f'Sending {commands[0][0]}')
client.send(commands[0][0])
if commands[0][1] is None:
commands.pop(0)
continue
else:
break
def send_command(c, handler = lambda reply: None):
turn.log.info(f'Scheduling {c}')
commands.append((c, handler))
if len(commands) == 1:
send_next_command()
def cleanout_networks(nets):
for line in nets.split('\n'):
if line.startswith('network id'):
continue
turn.log.info(f'Cleaning out old network: {repr(line)}')
send_command('REMOVE_NETWORK ' + line.split('\t')[0])
send_command('ADD_NETWORK', remember_network_id)
def remember_network_id(netid):
nonlocal active_network_id
active_network_id = netid
turn.log.info(f'Network ID: {active_network_id}')
def handle_scan_failure(reply):
if reply == 'FAIL':
send_command('TERMINATE')
send_command('SCAN', handle_scan_failure)
send_command('LEVEL 3')
send_command('LIST_NETWORKS', cleanout_networks)
def handle_event(e):
if e[0] == '<':
# It's an event
m = re.match(r'<(\d+)>(.*)', e)
if m:
(_level, message) = m.groups()
if message == 'CTRL-EVENT-SCAN-RESULTS':
send_command('SCAN_RESULTS')
turn.log.info(f'Level {_level}: {message}')
else:
turn.log.info(f'Unusual event: {e}')
else:
if len(commands) > 0:
turn.log.info(f'REPLY: {e}')
commands[0][1](e)
commands.pop(0)
send_next_command()
else:
turn.log.warning(f'Unexpected reply: {e}')
facet = turn.active_facet()
loop = find_loop()
turn.log.info('Starting background read thread')
threading.Thread(
name='background-wpa_supplicant-read-thread',
target=lambda: gather_events(facet, handle_event, client, loop)).start()

View File

@ -0,0 +1,5 @@
#!/usr/bin/env python3
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'python'))
import synit.daemon.user_settings_daemon

View File

@ -0,0 +1,5 @@
#!/usr/bin/env python3
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'python'))
import synit.daemon.wifi_daemon