From 46cb6bcbe8ed6254aa06ff155828666d14ef7bed Mon Sep 17 00:00:00 2001 From: littleblack111 Date: Sun, 14 Sep 2025 17:58:59 +0800 Subject: feat: network control --- src/client/connector.rs | 13 ++- src/client/connector/connectors/httpconnector.rs | 9 +- src/client/executor/tasks.rs | 108 +++++++++++++++++++++++ 3 files changed, 128 insertions(+), 2 deletions(-) (limited to 'src/client') diff --git a/src/client/connector.rs b/src/client/connector.rs index 6b59f60..3396349 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -5,6 +5,7 @@ use std::{thread::sleep, time::Duration}; use crate::executor::{Executor, tasks::IntoTask}; +#[derive(Debug)] pub(crate) enum Result { Ok(T), Err(E), @@ -17,16 +18,26 @@ pub(crate) trait Connection { // should use channel to notify update fn update(&self) -> Result; // None if no update(content is same) + // TODO: queue failed ntfy fn ntfy(&self, data: String) -> Result; fn is_prev(&mut self, str: &Self::Ok) -> bool; + fn force_network(&mut self); + + fn reinit(&mut self) { + sleep(Duration::from_millis(500)); + _ = Executor::new("network", ["on".to_string()]); + self.force_network(); + } + fn r#loop(&mut self) -> ! { loop { // tradeoff: if I put this at the ened of the loop, then the continue would just skip it, and continue doing update() until with no waiting, this wastes lot of cpu/IO // however, of course we can add a manual sleep before it continue and put this in the end, but that's code duplication // the cons of the current approch is that the first time we've entered, it would start sleeping - sleep(Duration::from_millis(500)); + self.reinit(); + match self.update() { Result::Ok(s) => { if self.is_prev(&s) { diff --git a/src/client/connector/connectors/httpconnector.rs b/src/client/connector/connectors/httpconnector.rs index fb97cc4..2d58d1d 100644 --- a/src/client/connector/connectors/httpconnector.rs +++ b/src/client/connector/connectors/httpconnector.rs @@ -3,10 +3,11 @@ use reqwest::{ blocking::{Client, Response}, }; -use crate::connector::*; +use crate::{connector::*, executor::tasks::TASKS}; use std::{thread::sleep, time::Duration}; pub(crate) struct HTTPConnector { + network_updater: Executor, pull_url: Url, ntfy_url: Url, client: Client, @@ -15,7 +16,9 @@ pub(crate) struct HTTPConnector { impl HTTPConnector { pub fn new(pull_url: &str, ntfy_url: &str) -> std::result::Result { + assert_eq!(TASKS[2].name, "network"); Ok(Self { + network_updater: Executor::new(&TASKS[2], ["on".to_string()]).unwrap(), prev: String::new(), pull_url: Url::parse(pull_url)?, ntfy_url: Url::parse(ntfy_url)?, @@ -88,4 +91,8 @@ impl Connection for HTTPConnector { } false } + + fn force_network(&mut self) { + _ = self.network_updater.run(); + } } diff --git a/src/client/executor/tasks.rs b/src/client/executor/tasks.rs index 4c8cdee..148e262 100644 --- a/src/client/executor/tasks.rs +++ b/src/client/executor/tasks.rs @@ -3,6 +3,12 @@ pub(crate) struct Task { pub exec: fn(&[String]) -> Result, } +impl PartialEq for &Task { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} + pub(crate) const TASKS: &[Task] = &[ Task { name: "nop", @@ -12,6 +18,10 @@ pub(crate) const TASKS: &[Task] = &[ name: "exec", exec: exec, }, + Task { + name: "network", + exec: network, + }, ]; fn no_op(_: &[String]) -> Result { @@ -37,6 +47,93 @@ fn exec(cmds: &[String]) -> Result { } } +fn network(switch: &[String]) -> Result { + use std::process::{Command, Stdio}; + let switch: bool = match switch[0].parse() { + Ok(s) => s, + Err(e) => return Err(e.to_string()), + }; + + // TODO: windows and linux + + // todo let .. = Command::new('networksetup') + #[cfg(target_os = "macos")] + { + use std::io::Read; + match Command::new("networksetup") + .arg("-listallhardwareports") + .stdout(Stdio::piped()) + .spawn() + { + Ok(devs) => { + match Command::new("awk") + .arg(r#"/Wi-Fi|AirPort/{getline; print $2}"#) + .stdin(Stdio::from(match devs.stdout { + Some(s) => s, + None => match devs.stderr { + Some(mut e) => { + let mut reason = String::new(); + match e.read_to_string(&mut reason) { + Ok(_) => {} + Err(e) => return Err(format!("networksetup failed to list hardware: {e}")) + } + return Err(format!( + "networksetup failed to list hardware: {reason}" + )); + } + None => Err("networksetup failed to list hardware".to_string())?, + }, + })) + .stdout(Stdio::piped()) + .spawn() + { + Ok(dev) => match Command::new("networksetup").arg("-setairportpower").arg( + match dev.stdout { + Some(mut o) => { + let mut output = String::new(); + match o.read_to_string(&mut output) { + Ok(_) => (), + Err(e) => return Err(format!("awk failed to get device from networksetup list hardware: {e}")) + }; + output + } + None => match dev.stderr { + Some(mut e) => { + let mut reason = String::new(); + match e.read_to_string(&mut reason) { + Ok(_) => {} + Err(e) => return Err(format!("awk failed to get device from networksetup list hardware: {e}")) + } + return Err(format!("awk failed to get device from networksetup list hardware: {reason}")); + } + None => Err("awk failed to get device from networksetup list hardware".to_string())?, + }, + }, + ).arg(if switch { "on" } else { "off" }).output() { + Ok(o) => { + if o.status.success() { + Ok(String::from_utf8(o.stdout) + .unwrap_or("Failed decoding vec -> String".to_string()) + .to_string()) + } else { + Err(String::from_utf8(o.stderr) + .unwrap_or("Failed decoding vec -> String".to_string()) + .to_string()) + } + } + Err(e) => Err(e.to_string()), + }, + Err(e) => Err(e.to_string()), + } + } + Err(e) => Err(e.to_string()), + } + } + + #[cfg(not(target_os = "macos"))] + Err("not implemented".to_string()) +} + #[derive(Debug)] pub(crate) enum IntoTaskErr { NotFound(String), @@ -67,3 +164,14 @@ impl IntoTask for &str { Err(IntoTaskErr::NotFound(self.to_string())) } } + +impl IntoTask for &Task { + fn into_task<'a>(self) -> Result<&'a Task, IntoTaskErr> { + for task in TASKS { + if task == self { + return Ok(task); + } + } + Err(IntoTaskErr::NotFound(self.name.to_string())) + } +} -- cgit v1.2.3