@ -1,30 +1,30 @@
/ / ! daemon watches network status and communicates updates to the GUI using uiwriter .
/ / ! daemon watches network status and communicates updates to the GUI using uiwriter .
/ / ! public fields are allocator
/ / ! usage example :
/ / ! usage example :
/ / !
/ / !
/ / ! var ctrl = try nif . wpa . Control . open ( " /run/wpa_supplicant/wlan0 " ) ;
/ / ! var nd = Daemon . init ( gpa , ngui_io_reader , ngui_io_writer , " /run/wpa_suppl/wlan0 " ) ;
/ / ! defer ctrl . close ( ) catch { } ;
/ / ! defer nd . deinit ( ) ;
/ / ! var nd : Daemon = . {
/ / ! . allocator = gpa ,
/ / ! . uiwriter = ngui_stdio_writer ,
/ / ! . wpa_ctrl = ctrl ,
/ / ! } ;
/ / ! try nd . start ( ) ;
/ / ! try nd . start ( ) ;
/ / ! / / wait for sigterm . . .
/ / ! nd . stop ( ) ;
/ / ! / / terminate ngui proc . . .
/ / ! nd . wait ( ) ;
/ / !
const builtin = @import ( " builtin " ) ;
const std = @import ( " std " ) ;
const std = @import ( " std " ) ;
const mem = std . mem ;
const mem = std . mem ;
const time = std . time ;
const time = std . time ;
const nif = @import ( " nif " ) ;
const comm = @import ( " ../comm.zig " ) ;
const comm = @import ( " ../comm.zig " ) ;
const network = @import ( " network.zig " ) ;
const screen = @import ( " ../ui/screen.zig " ) ;
const screen = @import ( " ../ui/screen.zig " ) ;
const types = @import ( " ../types.zig " ) ;
const types = @import ( " ../types.zig " ) ;
const SysService = @import ( " SysService.zig " ) ;
const SysService = @import ( " SysService.zig " ) ;
const logger = std . log . scoped ( . net mon) ;
const logger = std . log . scoped ( . dae mon) ;
allocator : mem . Allocator ,
allocator : mem . Allocator ,
uireader : std . fs . File . Reader , / / ngui stdout
uiwriter : std . fs . File . Writer , / / ngui stdin
uiwriter : std . fs . File . Writer , / / ngui stdin
wpa_ctrl : types . WpaControl , / / guarded by mu once start ' ed
wpa_ctrl : types . WpaControl , / / guarded by mu once start ' ed
@ -35,17 +35,20 @@ mu: std.Thread.Mutex = .{},
state : enum {
state : enum {
stopped ,
stopped ,
running ,
running ,
standby ,
poweroff ,
poweroff ,
} ,
} ,
main_thread : ? std . Thread = null ,
main_thread : ? std . Thread = null ,
comm_thread : ? std . Thread = null ,
poweroff_thread : ? std . Thread = null ,
poweroff_thread : ? std . Thread = null ,
want_stop : bool = false , / / tells daemon main loop to quit
want_stop : bool = false , / / tells daemon main loop to quit
want_network_report : bool = false ,
/ / network flags
want_wifi_scan : bool = false ,
want_network_report : bool , / / start gathering network status and send out as soon as ready
want_wifi_scan : bool , / / initiate wifi scan at the next loop cycle
network_report_ready : bool , / / indicates whether the network status is ready to be sent
wifi_scan_in_progress : bool = false ,
wifi_scan_in_progress : bool = false ,
network_report_ready : bool = true , / / no need to scan for an immediate report
wpa_save_config_on_connected : bool = false ,
wpa_save_config_on_connected : bool = false ,
/ / / system services actively managed by the daemon .
/ / / system services actively managed by the daemon .
@ -55,8 +58,10 @@ services: []SysService = &.{},
const Daemon = @This ( ) ;
const Daemon = @This ( ) ;
/ / / initializes a daemon instance using the provided GUI stdout reader and stdin writer ,
/ / / and a filesystem path to WPA control socket .
/ / / callers must deinit when done .
/ / / callers must deinit when done .
pub fn init ( a : std . mem . Allocator , iogui : std . fs . File . Writer , wpa_path : [ : 0 ] const u8 ) ! Daemon {
pub fn init ( a : std . mem . Allocator , r: std . fs . File . Reader , w : std . fs . File . Writer , wpa : [ : 0 ] const u8 ) ! Daemon {
var svlist = std . ArrayList ( SysService ) . init ( a ) ;
var svlist = std . ArrayList ( SysService ) . init ( a ) ;
errdefer {
errdefer {
for ( svlist . items ) | * sv | sv . deinit ( ) ;
for ( svlist . items ) | * sv | sv . deinit ( ) ;
@ -68,24 +73,21 @@ pub fn init(a: std.mem.Allocator, iogui: std.fs.File.Writer, wpa_path: [:0]const
try svlist . append ( SysService . init ( a , " bitcoind " , . { . stop_wait_sec = 600 } ) ) ;
try svlist . append ( SysService . init ( a , " bitcoind " , . { . stop_wait_sec = 600 } ) ) ;
return . {
return . {
. allocator = a ,
. allocator = a ,
. uiwriter = iogui ,
. uireader = r ,
. wpa_ctrl = try types . WpaControl . open ( wpa_path ) ,
. uiwriter = w ,
. wpa_ctrl = try types . WpaControl . open ( wpa ) ,
. state = . stopped ,
. state = . stopped ,
. services = svlist . toOwnedSlice ( ) ,
. services = svlist . toOwnedSlice ( ) ,
/ / send a network report right at start without wifi scan to make it faster .
. want_network_report = true ,
. want_wifi_scan = false ,
. network_report_ready = true ,
} ;
} ;
}
}
/ / / releases all associated resources .
/ / / releases all associated resources .
/ / / if the daemon is not in a stopped or poweroff mode , deinit panics .
/ / / the daemon must be stop ' ed and wait ' ed before deiniting .
pub fn deinit ( self : * Daemon ) void {
pub fn deinit ( self : * Daemon ) void {
self . mu . lock ( ) ;
defer self . mu . unlock ( ) ;
switch ( self . state ) {
. stopped , . poweroff = > if ( self . want_stop ) {
@panic ( " deinit while stopping " ) ;
} ,
else = > @panic ( " deinit while running " ) ,
}
self . wpa_ctrl . close ( ) catch | err | logger . err ( " deinit: wpa_ctrl.close: {any} " , . { err } ) ;
self . wpa_ctrl . close ( ) catch | err | logger . err ( " deinit: wpa_ctrl.close: {any} " , . { err } ) ;
for ( self . services ) | * sv | {
for ( self . services ) | * sv | {
sv . deinit ( ) ;
sv . deinit ( ) ;
@ -93,95 +95,124 @@ pub fn deinit(self: *Daemon) void {
self . allocator . free ( self . services ) ;
self . allocator . free ( self . services ) ;
}
}
/ / / start launches a main thread and returns immediately .
/ / / start launches daemon threads and returns immediately .
/ / / once started , the daemon must be eventually stop ' ed to clean up resources
/ / / once started , the daemon must be eventually stop ' ed and wait ' ed to clean up
/ / / even if a poweroff sequence is launched with beginPoweroff . however , in the latter
/ / / resources even if a poweroff sequence is initiated with beginPoweroff .
/ / / case the daemon cannot be start ' ed again after stop .
pub fn start ( self : * Daemon ) ! void {
pub fn start ( self : * Daemon ) ! void {
self . mu . lock ( ) ;
self . mu . lock ( ) ;
defer self . mu . unlock ( ) ;
defer self . mu . unlock ( ) ;
switch ( self . state ) {
switch ( self . state ) {
. running = > return error . AlreadyStarted ,
. poweroff = > return error . InPoweroffState ,
. stopped = > { } , / / continue
. stopped = > { } , / / continue
. poweroff = > return error . InPoweroffState ,
else = > return error . AlreadyStarted ,
}
}
try self . wpa_ctrl . attach ( ) ;
try self . wpa_ctrl . attach ( ) ;
errdefer {
self . wpa_ctrl . detach ( ) catch { } ;
self . want_stop = true ;
}
self . main_thread = try std . Thread . spawn ( . { } , mainThreadLoop , . { self } ) ;
self . main_thread = try std . Thread . spawn ( . { } , mainThreadLoop , . { self } ) ;
self . comm_thread = try std . Thread . spawn ( . { } , commThreadLoop , . { self } ) ;
self . state = . running ;
self . state = . running ;
}
}
/ / / stop blocks until all daemon threads exit , including poweroff if any .
/ / / tells the daemon to stop threads to prepare for termination .
/ / / once stopped , the daemon can be start ' ed again unless a poweroff was initiated .
/ / / stop returns immediately .
/ / /
/ / / callers must ` wait ` to release all resources .
/ / / note : stop leaves system services like lnd and bitcoind running .
pub fn stop ( self : * Daemon ) void {
pub fn stop ( self : * Daemon ) void {
self . mu . lock ( ) ;
self . mu . lock ( ) ;
if ( self . want_stop or self . state = = . stopped ) {
defer self . mu . unlock ( ) ;
self . mu . unlock ( ) ;
return ; / / already in progress or stopped
}
self . want_stop = true ;
self . want_stop = true ;
self . mu . unlock ( ) ; / / avoid threads deadlock
}
/ / / blocks and waits for all threads to terminate . the daemon instance cannot
/ / / be start ' ed afterwards .
/ / /
/ / / note that in order for wait to return , the GUI I / O reader provided at init
/ / / must be closed .
pub fn wait ( self : * Daemon ) void {
if ( self . main_thread ) | th | {
if ( self . main_thread ) | th | {
th . join ( ) ;
th . join ( ) ;
self . main_thread = null ;
self . main_thread = null ;
}
}
if ( self . comm_thread ) | th | {
th . join ( ) ;
self . comm_thread = null ;
}
/ / must be the last one to join because it sends a final poweroff report .
/ / must be the last one to join because it sends a final poweroff report .
if ( self . poweroff_thread ) | th | {
if ( self . poweroff_thread ) | th | {
th . join ( ) ;
th . join ( ) ;
self . poweroff_thread = null ;
self . poweroff_thread = null ;
}
}
self . mu . lock ( ) ;
self . wpa_ctrl . detach ( ) catch | err | logger . err ( " wait: wpa_ctrl.detach: {any} " , . { err } ) ;
defer self . mu . unlock ( ) ;
self . want_stop = false ;
self . want_stop = false ;
if ( self . state ! = . poweroff ) { / / keep poweroff to prevent start ' ing again
self . state = . stopped ;
self . state = . stopped ;
}
}
self . wpa_ctrl . detach ( ) catch | err | logger . err ( " stop: wpa_ctrl.detach: {any} " , . { err } ) ;
}
pub fn standby ( self : * Daemon ) ! void {
/ / / tells the daemon to go into a standby mode , typically due to user inactivity .
fn standby ( self : * Daemon ) ! void {
self . mu . lock ( ) ;
self . mu . lock ( ) ;
defer self . mu . unlock ( ) ;
defer self . mu . unlock ( ) ;
switch ( self . state ) {
switch ( self . state ) {
. poweroff = > return error . InPoweroffState ,
. standby = > { } ,
. running, . stopped = > { } , / / continue
. stopped, . poweroff = > return error . InvalidState ,
}
. running = > {
try screen . backlight ( . off ) ;
try screen . backlight ( . off ) ;
self . state = . standby ;
} ,
}
}
}
pub fn wakeup ( _ : * Daemon ) ! void {
/ / / tells the daemon to return from standby , typically due to user interaction .
fn wakeup ( self : * Daemon ) ! void {
self . mu . lock ( ) ;
defer self . mu . unlock ( ) ;
switch ( self . state ) {
. running = > { } ,
. stopped , . poweroff = > return error . InvalidState ,
. standby = > {
try screen . backlight ( . on ) ;
try screen . backlight ( . on ) ;
self . state = . running ;
} ,
}
}
}
/ / / initiates system poweroff sequence in a separate thread : shut down select
/ / / initiates system poweroff sequence in a separate thread : shut down select
/ / / system services such as lnd and bitcoind , and issue " poweroff " command .
/ / / system services such as lnd and bitcoind , and issue " poweroff " command .
/ / /
/ / /
/ / / in the poweroff mode , the daemon is still running as usual and must be stop ' ed .
/ / / beingPoweroff also makes other threads exit but callers must still call ` wait `
/ / / however , in poweroff mode regular functionalities are disabled , such as
/ / / to make sure poweroff sequence is complete .
/ / / wifi scan and standby .
fn beginPoweroff ( self : * Daemon ) ! void {
pub fn beginPoweroff ( self : * Daemon ) ! void {
self . mu . lock ( ) ;
self . mu . lock ( ) ;
defer self . mu . unlock ( ) ;
defer self . mu . unlock ( ) ;
if ( self . state = = . poweroff ) {
switch ( self . state ) {
return ; / / already in poweroff stat e
. poweroff = > { } , / / already in poweroff mod e
}
. stopped = > return error . InvalidState ,
. running , . standby = > {
self . poweroff_thread = try std . Thread . spawn ( . { } , poweroffThread , . { self } ) ;
self . poweroff_thread = try std . Thread . spawn ( . { } , poweroffThread , . { self } ) ;
self . state = . poweroff ;
self . state = . poweroff ;
self . want_stop = true ;
} ,
}
}
}
/ / stops all monitored services and issue poweroff command while reporting
/ / / set when poweroff_thread starts . available in tests only .
/ / the progress to ngui .
var test_poweroff_started = if ( builtin . is_test ) std . Thread . ResetEvent { } else { } ;
fn poweroffThread ( self : * Daemon ) ! void {
/ / / the poweroff thread entry point : stops all monitored services and issues poweroff
/ / / command while reporting the progress to ngui .
/ / / exits after issuing " poweroff " command .
fn poweroffThread ( self : * Daemon ) void {
if ( builtin . is_test ) {
test_poweroff_started . set ( ) ;
}
logger . info ( " begin powering off " , . { } ) ;
logger . info ( " begin powering off " , . { } ) ;
screen . backlight ( . on ) catch | err | {
screen . backlight ( . on ) catch | err | {
logger . err ( " screen.backlight(.on) during poweroff: {any} " , . { err } ) ;
logger . err ( " screen.backlight(.on) during poweroff: {any} " , . { err } ) ;
} ;
} ;
self . wpa_ctrl . detach ( ) catch { } ; / / don ' t care because powering off anyway
/ / initiate shutdown of all services concurrently .
/ / initiate shutdown of all services concurrently .
for ( self . services ) | * sv | {
for ( self . services ) | * sv | {
@ -202,27 +233,25 @@ fn poweroffThread(self: *Daemon) !void {
logger . info ( " poweroff: {any} " , . { res } ) ;
logger . info ( " poweroff: {any} " , . { res } ) ;
}
}
/ / / main thread entry point .
/ / / main thread entry point : watches for want_xxx flags and monitors network .
fn mainThreadLoop ( self : * Daemon ) ! void {
/ / / exits when want_stop is true .
fn mainThreadLoop ( self : * Daemon ) void {
var quit = false ;
var quit = false ;
while ( ! quit ) {
while ( ! quit ) {
self . mainThreadLoopCycle ( ) catch | err | logger . err ( " main thread loop: {any} " , . { err } ) ;
self . mainThreadLoopCycle ( ) catch | err | logger . err ( " main thread loop: {any} " , . { err } ) ;
std . atomic . spinLoopHint ( ) ;
time . sleep ( 1 * time . ns_per_s ) ;
time . sleep ( 1 * time . ns_per_s ) ;
self . mu . lock ( ) ;
self . mu . lock ( ) ;
quit = self . want_stop ;
quit = self . want_stop ;
self . mu . unlock ( ) ;
self . mu . unlock ( ) ;
}
}
logger . info ( " exiting main thread loop " , . { } ) ;
}
}
/ / / run one cycle of the main thread loop iteration .
/ / / run s one cycle of the main thread loop iteration .
/ / / unless in poweroff mode , the cycle holds self . mu for the whole duration .
/ / / the cycle holds self . mu for the whole duration .
fn mainThreadLoopCycle ( self : * Daemon ) ! void {
fn mainThreadLoopCycle ( self : * Daemon ) ! void {
switch ( self . state ) {
/ / poweroff mode : do nothing ; handled by poweroffThread
. poweroff = > { } ,
/ / normal state : running or standby
else = > {
self . mu . lock ( ) ;
self . mu . lock ( ) ;
defer self . mu . unlock ( ) ;
defer self . mu . unlock ( ) ;
self . readWPACtrlMsg ( ) catch | err | logger . err ( " readWPACtrlMsg: {any} " , . { err } ) ;
self . readWPACtrlMsg ( ) catch | err | logger . err ( " readWPACtrlMsg: {any} " , . { err } ) ;
@ -234,16 +263,80 @@ fn mainThreadLoopCycle(self: *Daemon) !void {
}
}
}
}
if ( self . want_network_report and self . network_report_ready ) {
if ( self . want_network_report and self . network_report_ready ) {
if ( self . sendNetworkReport ( ) ) {
if ( network . sendReport ( self . allocator , & self . wpa_ctrl , self . uiwriter ) ) {
self . want_network_report = false ;
self . want_network_report = false ;
} else | err | {
} else | err | {
logger . err ( " se ndN etworkReport: {any}" , . { err } ) ;
logger . err ( " network.send Report: {any}" , . { err } ) ;
}
}
}
}
}
/ / / comm thread entry point : reads messages sent from ngui and acts accordinly .
/ / / exits when want_stop is true or comm reader is closed .
/ / / note : the thread might not exit immediately on want_stop because comm . read
/ / / is blocking .
fn commThreadLoop ( self : * Daemon ) void {
var quit = false ;
loop : while ( ! quit ) {
std . atomic . spinLoopHint ( ) ;
time . sleep ( 100 * time . ns_per_ms ) ;
const msg = comm . read ( self . allocator , self . uireader ) catch | err | {
self . mu . lock ( ) ;
defer self . mu . unlock ( ) ;
if ( self . want_stop ) {
break : loop ; / / pipe is most likely already closed
}
switch ( self . state ) {
. stopped , . poweroff = > break : loop ,
. running , . standby = > {
logger . err ( " commThreadLoop: {any} " , . { err } ) ;
if ( err = = error . EndOfStream ) {
/ / pointless to continue running if comms I / O is broken .
self . want_stop = true ;
break : loop ;
}
continue ;
} ,
} ,
}
}
} ;
logger . debug ( " got msg: {s} " , . { @tagName ( msg ) } ) ;
switch ( msg ) {
. pong = > {
logger . info ( " received pong from ngui " , . { } ) ;
} ,
. poweroff = > {
self . beginPoweroff ( ) catch | err | logger . err ( " beginPoweroff: {any} " , . { err } ) ;
} ,
. get_network_report = > | req | {
self . reportNetworkStatus ( . { . scan = req . scan } ) ;
} ,
. wifi_connect = > | req | {
self . startConnectWifi ( req . ssid , req . password ) catch | err | {
logger . err ( " startConnectWifi: {any} " , . { err } ) ;
} ;
} ,
. standby = > {
logger . info ( " entering standby mode " , . { } ) ;
self . standby ( ) catch | err | logger . err ( " nd.standby: {any} " , . { err } ) ;
} ,
. wakeup = > {
logger . info ( " wakeup from standby " , . { } ) ;
self . wakeup ( ) catch | err | logger . err ( " nd.wakeup: {any} " , . { err } ) ;
} ,
else = > logger . warn ( " unhandled msg tag {s} " , . { @tagName ( msg ) } ) ,
}
comm . free ( self . allocator , msg ) ;
self . mu . lock ( ) ;
quit = self . want_stop ;
self . mu . unlock ( ) ;
}
logger . info ( " exiting comm thread loop " , . { } ) ;
}
}
/ / / sends poweroff progress to uiwriter in comm . Message . PoweroffProgress format .
fn sendPoweroffReport ( self : * Daemon ) ! void {
fn sendPoweroffReport ( self : * Daemon ) ! void {
var svstat = try self . allocator . alloc ( comm . Message . PoweroffProgress . Service , self . services . len ) ;
var svstat = try self . allocator . alloc ( comm . Message . PoweroffProgress . Service , self . services . len ) ;
defer self . allocator . free ( svstat ) ;
defer self . allocator . free ( svstat ) ;
@ -289,18 +382,20 @@ fn wifiConnected(self: *Daemon) void {
}
}
/ / / invoked when CTRL - EVENT - SSID - TEMP - DISABLED event with authentication failures is seen .
/ / / invoked when CTRL - EVENT - SSID - TEMP - DISABLED event with authentication failures is seen .
/ / / caller must hold self . mu .
/ / / caller s must hold self . mu .
fn wifiInvalidKey ( self : * Daemon ) void {
fn wifiInvalidKey ( self : * Daemon ) void {
self . wpa_save_config_on_connected = false ;
self . wpa_save_config_on_connected = false ;
self . want_network_report = true ;
self . want_network_report = true ;
self . network_report_ready = true ;
self . network_report_ready = true ;
}
}
pub const ReportNetworkStatusOpt = struct {
const ReportNetworkStatusOpt = struct {
scan : bool ,
scan : bool ,
} ;
} ;
pub fn reportNetworkStatus ( self : * Daemon , opt : ReportNetworkStatusOpt ) void {
/ / / tells the daemon to start preparing network status report , including a wifi
/ / / scan as an option .
fn reportNetworkStatus ( self : * Daemon , opt : ReportNetworkStatusOpt ) void {
self . mu . lock ( ) ;
self . mu . lock ( ) ;
defer self . mu . unlock ( ) ;
defer self . mu . unlock ( ) ;
self . want_network_report = true ;
self . want_network_report = true ;
@ -310,7 +405,8 @@ pub fn reportNetworkStatus(self: *Daemon, opt: ReportNetworkStatusOpt) void {
}
}
}
}
pub fn startConnectWifi ( self : * Daemon , ssid : [ ] const u8 , password : [ ] const u8 ) ! void {
/ / / initiates wifi connection procedure in a separate thread
fn startConnectWifi ( self : * Daemon , ssid : [ ] const u8 , password : [ ] const u8 ) ! void {
if ( ssid . len = = 0 ) {
if ( ssid . len = = 0 ) {
return error . ConnectWifiEmptySSID ;
return error . ConnectWifiEmptySSID ;
}
}
@ -320,21 +416,24 @@ pub fn startConnectWifi(self: *Daemon, ssid: []const u8, password: []const u8) !
th . detach ( ) ;
th . detach ( ) ;
}
}
/ / / the wifi connection procedure thread entry point .
/ / / holds self . mu for the whole duration . however the thread lifetime is expected
/ / / to be short since all it does is issuing commands to self . wpa_ctrl .
/ / /
/ / / the thread owns ssid and password args , and frees them at exit .
fn connectWifiThread ( self : * Daemon , ssid : [ ] const u8 , password : [ ] const u8 ) void {
fn connectWifiThread ( self : * Daemon , ssid : [ ] const u8 , password : [ ] const u8 ) void {
self . mu . lock ( ) ;
defer {
defer {
self . mu . unlock ( ) ;
self . allocator . free ( ssid ) ;
self . allocator . free ( ssid ) ;
self . allocator . free ( password ) ;
self . allocator . free ( password ) ;
}
}
/ / https : / / hostap . epitest . fi / wpa_supplicant / devel / ctrl_iface_page . html
/ / https : / / hostap . epitest . fi / wpa_supplicant / devel / ctrl_iface_page . html
/ / https : / / wiki . archlinux . org / title / WPA_supplicant
/ / https : / / wiki . archlinux . org / title / WPA_supplicant
/ / this prevents main thread from looping until released ,
const id = network . addWifi ( self . allocator , & self . wpa_ctrl , ssid , password ) catch | err | {
/ / but the following commands and expected to be pretty quick .
logger . err ( " addWifi: {any}; exiting " , . { err } ) ;
self . mu . lock ( ) ;
defer self . mu . unlock ( ) ;
const id = self . addWifiNetwork ( ssid , password ) catch | err | {
logger . err ( " addWifiNetwork: {any}; exiting " , . { err } ) ;
return ;
return ;
} ;
} ;
/ / SELECT_NETWORK < id > - this disables others
/ / SELECT_NETWORK < id > - this disables others
@ -353,51 +452,8 @@ fn connectWifiThread(self: *Daemon, ssid: []const u8, password: []const u8) void
self . wpa_save_config_on_connected = true ;
self . wpa_save_config_on_connected = true ;
}
}
/ / / adds a new network and configures its parameters .
/ / / reads all available messages from self . wpa_ctrl and acts accordingly .
/ / / caller must hold self . mu .
/ / / callers must hold self . mu .
fn addWifiNetwork ( self : * Daemon , ssid : [ ] const u8 , password : [ ] const u8 ) ! u32 {
/ / - ADD_NETWORK - > get id and set parameters
/ / - SET_NETWORK < id > ssid " ssid "
/ / - if password :
/ / SET_NETWORK < id > psk " password "
/ / else :
/ / SET_NETWORK < id > key_mgmt NONE
const newWifiId = try self . wpa_ctrl . addNetwork ( ) ;
errdefer self . wpa_ctrl . removeNetwork ( newWifiId ) catch | err | {
logger . err ( " addWifiNetwork cleanup: {any} " , . { err } ) ;
} ;
var buf : [ 128 : 0 ] u8 = undefined ;
/ / TODO : convert ssid to hex string , to support special characters
const ssidZ = try std . fmt . bufPrintZ ( & buf , " \" {s} \" " , . { ssid } ) ;
try self . wpa_ctrl . setNetworkParam ( newWifiId , " ssid " , ssidZ ) ;
if ( password . len > 0 ) {
/ / TODO : switch to wpa_passphrase
const v = try std . fmt . bufPrintZ ( & buf , " \" {s} \" " , . { password } ) ;
try self . wpa_ctrl . setNetworkParam ( newWifiId , " psk " , v ) ;
} else {
try self . wpa_ctrl . setNetworkParam ( newWifiId , " key_mgmt " , " NONE " ) ;
}
/ / - LIST_NETWORKS : network id / ssid / bssid / flags
/ / - for each matching ssid unless it ' s newly created : REMOVE_NETWORK < id >
if ( self . queryWifiNetworksList ( . { . ssid = ssid } ) ) | res | {
defer self . allocator . free ( res ) ;
for ( res ) | id | {
if ( id = = newWifiId ) {
continue ;
}
self . wpa_ctrl . removeNetwork ( id ) catch | err | {
logger . err ( " wpa_ctrl.removeNetwork({}): {any} " , . { id , err } ) ;
} ;
}
} else | err | {
logger . err ( " queryWifiNetworksList({s}): {any}; won't remove existing, if any " , . { ssid , err } ) ;
}
return newWifiId ;
}
/ / / caller must hold self . mu .
fn readWPACtrlMsg ( self : * Daemon ) ! void {
fn readWPACtrlMsg ( self : * Daemon ) ! void {
var buf : [ 512 : 0 ] u8 = undefined ;
var buf : [ 512 : 0 ] u8 = undefined ;
while ( try self . wpa_ctrl . pending ( ) ) {
while ( try self . wpa_ctrl . pending ( ) ) {
@ -428,177 +484,31 @@ fn readWPACtrlMsg(self: *Daemon) !void {
}
}
}
}
/ / / report network status to ngui .
/ / / caller must hold self . mu .
fn sendNetworkReport ( self : * Daemon ) ! void {
var report = comm . Message . NetworkReport {
. ipaddrs = undefined ,
. wifi_ssid = null ,
. wifi_scan_networks = undefined ,
} ;
/ / fetch all public IP addresses using getifaddrs
const pubaddr = try nif . pubAddresses ( self . allocator , null ) ;
defer self . allocator . free ( pubaddr ) ;
/ / var addrs = std . ArrayList ( [ ] ) . init ( t . allocator ) ;
var ipaddrs = try self . allocator . alloc ( [ ] const u8 , pubaddr . len ) ;
for ( pubaddr ) | a , i | {
ipaddrs [ i ] = try std . fmt . allocPrint ( self . allocator , " {s} " , . { a } ) ;
}
defer {
for ( ipaddrs ) | a | self . allocator . free ( a ) ;
self . allocator . free ( ipaddrs ) ;
}
report . ipaddrs = ipaddrs ;
/ / get currently connected SSID , if any , from WPA ctrl
const ssid = self . queryWifiSSID ( ) catch | err | blk : {
logger . err ( " queryWifiSsid: {any} " , . { err } ) ;
break : blk null ;
} ;
defer if ( ssid ) | v | self . allocator . free ( v ) ;
report . wifi_ssid = ssid ;
/ / fetch available wifi networks from scan results using WPA ctrl
var wifi_networks : ? StringList = if ( self . queryWifiScanResults ( ) ) | v | v else | err | blk : {
logger . err ( " queryWifiScanResults: {any} " , . { err } ) ;
break : blk null ;
} ;
defer if ( wifi_networks ) | * list | list . deinit ( ) ;
if ( wifi_networks ) | list | {
report . wifi_scan_networks = list . items ( ) ;
}
/ / report everything back to ngui
return comm . write ( self . allocator , self . uiwriter , comm . Message { . network_report = report } ) ;
}
/ / / caller must hold self . mu .
fn queryWifiSSID ( self : * Daemon ) ! ? [ ] const u8 {
var buf : [ 512 : 0 ] u8 = undefined ;
const resp = try self . wpa_ctrl . request ( " STATUS " , & buf , null ) ;
const ssid = " ssid= " ;
var it = mem . tokenize ( u8 , resp , " \n " ) ;
while ( it . next ( ) ) | line | {
if ( mem . startsWith ( u8 , line , ssid ) ) {
/ / TODO : check line . len vs ssid . len
const v = try self . allocator . dupe ( u8 , line [ ssid . len . . ] ) ;
return v ;
}
}
return null ;
}
/ / / caller must hold self . mu .
/ / / the retuned value must free ' d with StringList . deinit .
fn queryWifiScanResults ( self : * Daemon ) ! StringList {
var buf : [ 8192 : 0 ] u8 = undefined ; / / TODO : what if isn ' t enough ?
/ / first line is banner : " bssid / frequency / signal level / flags / ssid "
const resp = try self . wpa_ctrl . request ( " SCAN_RESULTS " , & buf , null ) ;
var it = mem . tokenize ( u8 , resp , " \n " ) ;
if ( it . next ( ) = = null ) {
return error . MissingWifiScanHeader ;
}
var seen = std . BufSet . init ( self . allocator ) ;
defer seen . deinit ( ) ;
var list = StringList . init ( self . allocator ) ;
errdefer list . deinit ( ) ;
while ( it . next ( ) ) | line | {
/ / TODO : wpactrl ' s text protocol won ' t work for names with control characters
if ( mem . lastIndexOfScalar ( u8 , line , '\t' ) ) | i | {
const s = mem . trim ( u8 , line [ i . . ] , " \t \n " ) ;
if ( s . len = = 0 or seen . contains ( s ) ) {
continue ;
}
try seen . insert ( s ) ;
try list . append ( s ) ;
}
}
return list ;
}
const WifiNetworksListFilter = struct {
ssid : ? [ ] const u8 , / / ignore networks whose ssid doesn ' t match
} ;
/ / / caller must hold self . mu .
/ / / the returned value must be free ' d with self . allocator .
fn queryWifiNetworksList ( self : * Daemon , filter : WifiNetworksListFilter ) ! [ ] u32 {
var buf : [ 8192 : 0 ] u8 = undefined ; / / TODO : is this enough ?
/ / first line is banner : " network id / ssid / bssid / flags "
const resp = try self . wpa_ctrl . request ( " LIST_NETWORKS " , & buf , null ) ;
var it = mem . tokenize ( u8 , resp , " \n " ) ;
if ( it . next ( ) = = null ) {
return error . MissingWifiNetworksListHeader ;
}
var list = std . ArrayList ( u32 ) . init ( self . allocator ) ;
while ( it . next ( ) ) | line | {
var cols = mem . tokenize ( u8 , line , " \t " ) ;
const id_str = cols . next ( ) orelse continue ; / / bad line format ?
const ssid = cols . next ( ) orelse continue ; / / bad line format ?
const id = std . fmt . parseUnsigned ( u32 , id_str , 10 ) catch continue ; / / skip bad line
if ( filter . ssid ! = null and ! mem . eql ( u8 , filter . ssid . ? , ssid ) ) {
continue ;
}
list . append ( id ) catch { } ; / / grab anything we can
}
return list . toOwnedSlice ( ) ;
}
/ / TODO : turns this into a UniqStringList backed by StringArrayHashMap ; also see std . BufSet
const StringList = struct {
l : std . ArrayList ( [ ] const u8 ) ,
allocator : mem . Allocator ,
const Self = @This ( ) ;
pub fn init ( allocator : mem . Allocator ) Self {
return Self {
. l = std . ArrayList ( [ ] const u8 ) . init ( allocator ) ,
. allocator = allocator ,
} ;
}
pub fn deinit ( self : * Self ) void {
for ( self . l . items ) | a | {
self . allocator . free ( a ) ;
}
self . l . deinit ( ) ;
}
pub fn append ( self : * Self , s : [ ] const u8 ) ! void {
const item = try self . allocator . dupe ( u8 , s ) ;
errdefer self . allocator . free ( item ) ;
try self . l . append ( item ) ;
}
pub fn items ( self : Self ) [ ] const [ ] const u8 {
return self . l . items ;
}
} ;
test " start-stop " {
test " start-stop " {
const t = std . testing ;
const t = std . testing ;
const pipe = try types . IoPipe . create ( ) ;
const pipe = try types . IoPipe . create ( ) ;
defer pipe . close ( ) ;
var daemon = try Daemon . init ( t . allocator , pipe . reader ( ) , pipe . writer ( ) , " /dev/null " ) ;
var daemon = try Daemon . init ( t . allocator , pipe . writer ( ) , " /dev/null " ) ;
daemon . want_network_report = false ;
try t . expect ( daemon . state = = . stopped ) ;
try t . expect ( daemon . state = = . stopped ) ;
try daemon . start ( ) ;
try daemon . start ( ) ;
try t . expect ( daemon . state = = . running ) ;
try t . expect ( daemon . state = = . running ) ;
try t . expect ( daemon . main_thread ! = null ) ;
try t . expect ( daemon . comm_thread ! = null ) ;
try t . expect ( daemon . poweroff_thread = = null ) ;
try t . expect ( daemon . wpa_ctrl . opened ) ;
try t . expect ( daemon . wpa_ctrl . opened ) ;
try t . expect ( daemon . wpa_ctrl . attached ) ;
try t . expect ( daemon . wpa_ctrl . attached ) ;
daemon . stop ( ) ;
daemon . stop ( ) ;
pipe . close ( ) ;
daemon . wait ( ) ;
try t . expect ( daemon . state = = . stopped ) ;
try t . expect ( daemon . state = = . stopped ) ;
try t . expect ( ! daemon . want_stop ) ;
try t . expect ( ! daemon . wpa_ctrl . attached ) ;
try t . expect ( daemon . wpa_ctrl . opened ) ;
try t . expect ( daemon . main_thread = = null ) ;
try t . expect ( daemon . main_thread = = null ) ;
try t . expect ( daemon . comm_thread = = null ) ;
try t . expect ( daemon . poweroff_thread = = null ) ;
try t . expect ( daemon . poweroff_thread = = null ) ;
try t . expect ( ! daemon . wpa_ctrl . attached ) ;
try t . expect ( daemon . wpa_ctrl . opened ) ;
try t . expect ( daemon . services . len > 0 ) ;
try t . expect ( daemon . services . len > 0 ) ;
for ( daemon . services ) | * sv | {
for ( daemon . services ) | * sv | {
@ -610,7 +520,7 @@ test "start-stop" {
try t . expect ( ! daemon . wpa_ctrl . opened ) ;
try t . expect ( ! daemon . wpa_ctrl . opened ) ;
}
}
test " start-poweroff -stop " {
test " start-poweroff " {
const t = std . testing ;
const t = std . testing ;
const tt = @import ( " ../test.zig " ) ;
const tt = @import ( " ../test.zig " ) ;
@ -618,37 +528,44 @@ test "start-poweroff-stop" {
defer arena_alloc . deinit ( ) ;
defer arena_alloc . deinit ( ) ;
const arena = arena_alloc . allocator ( ) ;
const arena = arena_alloc . allocator ( ) ;
const pipe = try types . IoPipe . create ( ) ;
const gui_stdin = try types . IoPipe . create ( ) ;
var daemon = try Daemon . init ( arena , pipe . writer ( ) , " /dev/null " ) ;
const gui_stdout = try types . IoPipe . create ( ) ;
const gui_reader = gui_stdin . reader ( ) ;
var daemon = try Daemon . init ( arena , gui_stdout . reader ( ) , gui_stdin . writer ( ) , " /dev/null " ) ;
daemon . want_network_report = false ;
defer {
defer {
daemon . deinit ( ) ;
daemon . deinit ( ) ;
pipe . close ( ) ;
gui_stdin . close ( ) ;
}
}
try daemon . start ( ) ;
try daemon . start ( ) ;
try daemon. beginPoweroff ( ) ;
try comm. write ( arena , gui_stdout . writer ( ) , comm . Message . poweroff ) ;
daemon . stop ( ) ;
try test_poweroff_started . timedWait ( 2 * time . ns_per_s ) ;
try t . expect ( daemon . state = = . poweroff ) ;
try t . expect ( daemon . state = = . poweroff ) ;
gui_stdout . close ( ) ;
daemon . wait ( ) ;
try t . expect ( daemon . state = = . stopped ) ;
try t . expect ( daemon . poweroff_thread = = null ) ;
for ( daemon . services ) | * sv | {
for ( daemon . services ) | * sv | {
try t . expect ( sv . stop_proc . spawned ) ;
try t . expect ( sv . stop_proc . spawned ) ;
try t . expect ( sv . stop_proc . waited ) ;
try t . expect ( sv . stop_proc . waited ) ;
try t . expectEqual ( SysService . Status . stopped , sv . status ( ) ) ;
try t . expectEqual ( SysService . Status . stopped , sv . status ( ) ) ;
}
}
const pipe_reader = pipe . reader ( ) ;
const msg1 = try comm . read ( arena , gui_reader ) ;
const msg1 = try comm . read ( arena , pipe_reader ) ;
try tt . expectDeepEqual ( comm . Message { . poweroff_progress = . { . services = & . {
try tt . expectDeepEqual ( comm . Message { . poweroff_progress = . { . services = & . {
. { . name = " lnd " , . stopped = false , . err = null } ,
. { . name = " lnd " , . stopped = false , . err = null } ,
. { . name = " bitcoind " , . stopped = false , . err = null } ,
. { . name = " bitcoind " , . stopped = false , . err = null } ,
} } } , msg1 ) ;
} } } , msg1 ) ;
const msg2 = try comm . read ( arena , pipe _reader) ;
const msg2 = try comm . read ( arena , gui _reader) ;
try tt . expectDeepEqual ( comm . Message { . poweroff_progress = . { . services = & . {
try tt . expectDeepEqual ( comm . Message { . poweroff_progress = . { . services = & . {
. { . name = " lnd " , . stopped = true , . err = null } ,
. { . name = " lnd " , . stopped = true , . err = null } ,
. { . name = " bitcoind " , . stopped = false , . err = null } ,
. { . name = " bitcoind " , . stopped = false , . err = null } ,
} } } , msg2 ) ;
} } } , msg2 ) ;
const msg3 = try comm . read ( arena , pipe _reader) ;
const msg3 = try comm . read ( arena , gui _reader) ;
try tt . expectDeepEqual ( comm . Message { . poweroff_progress = . { . services = & . {
try tt . expectDeepEqual ( comm . Message { . poweroff_progress = . { . services = & . {
. { . name = " lnd " , . stopped = true , . err = null } ,
. { . name = " lnd " , . stopped = true , . err = null } ,
. { . name = " bitcoind " , . stopped = true , . err = null } ,
. { . name = " bitcoind " , . stopped = true , . err = null } ,