1#![allow(rustdoc::private_intra_doc_links)]
8
9use anyhow::{Context, Result};
10use customlog::{ConsoleLogger, FileLogger, FlushGuard, RotateOptions, RotateSize};
11use getopts::Options;
12use log::{LevelFilter, error, info};
13use std::env;
14use sys::sysmod::SystemModules;
15use sys::taskserver::{Control, RunResult, TaskServer};
16
17const FILE_LOG: &str = "shanghai.log";
19const LOG_FILTER: &[&str] = &[module_path!(), "sys"];
20const LOG_ROTATE_SIZE: usize = 1024 * 1024;
21const LOG_ROTATE_COUNT: u16 = 10;
22const LOG_BUF_SIZE: usize = 64 * 1024;
23
24const FILE_SYSTEMD_SERVICE: &str = "shanghai.service";
25
26fn log_target_filter(target: &str) -> bool {
27 LOG_FILTER.iter().any(|filter| target.starts_with(filter))
28}
29
30fn init_log(verbose: bool) -> Result<FlushGuard> {
40 let rotate_opts = RotateOptions {
42 size: RotateSize::Enabled(LOG_ROTATE_SIZE),
43 file_count: LOG_ROTATE_COUNT,
44 ..Default::default()
45 };
46
47 let log_dir = utils::dir::cache_dir()?;
48 let file_path = log_dir.join(FILE_LOG);
49 let file_log = FileLogger::new_boxed(
50 LevelFilter::Info,
51 log_target_filter,
52 customlog::default_formatter,
53 &file_path,
54 LOG_BUF_SIZE,
55 rotate_opts,
56 )?;
57 let file_path = file_path.canonicalize()?;
58
59 let console_filter = if cfg!(debug_assertions) || verbose {
61 LevelFilter::Trace
62 } else {
63 LevelFilter::Info
64 };
65 let console_log = ConsoleLogger::new_boxed(
66 customlog::Console::Stdout,
67 console_filter,
68 log_target_filter,
69 customlog::default_formatter,
70 );
71 let loggers = vec![console_log, file_log];
72
73 let guard = customlog::init(loggers, LevelFilter::Trace);
74 info!("init log: {}", file_path.to_string_lossy());
75
76 Ok(guard)
77}
78
79async fn boot_msg_task(ctrl: Control) -> Result<()> {
81 let build_info = verinfo::version_info();
82 let now = chrono::Local::now();
84 let now = now.format("%F %T %:z");
85 let msg = format!("[{now}] Boot...\n{build_info}");
86
87 {
95 let mut discord = ctrl.sysmods().discord.lock().await;
96 if let Err(why) = discord.say(&msg).await {
97 error!("error on discord notification");
98 error!("{why:#?}");
99 }
100 }
101
102 Ok(())
103}
104
105fn system_main() -> Result<()> {
114 let config_dir = utils::dir::config_dir()?;
115
116 let sigusr1 = || {
117 info!("Flush log");
118 log::logger().flush();
119 None
120 };
121 let sigusr2 = || None;
122
123 loop {
124 info!("system main");
125 info!("{}", verinfo::version_info());
126 log::logger().flush();
127
128 sys::config::load(&config_dir)?;
129
130 let sysmods = SystemModules::new()?;
131 let ts = TaskServer::new(sysmods);
132
133 ts.spawn_oneshot_task("boot_msg", boot_msg_task);
134 let run_result = ts.run(sigusr1, sigusr2);
135
136 info!("task server dropped");
137
138 match run_result {
139 RunResult::Shutdown => {
140 info!("result: shutdown");
141 break;
142 }
143 RunResult::Reboot => {
144 info!("result: reboot");
145 }
146 }
147 }
148
149 Ok(())
150}
151
152fn create_systemd_files() -> Result<()> {
153 let dir = utils::dir::share_dir()?;
154 let file_path = dir.join(FILE_SYSTEMD_SERVICE);
155
156 let exe = env::current_exe()?;
157 let exe = exe.to_str().context("Invalid UTF-8")?;
158 let cd = env::current_dir()?;
159 let cd = cd.to_str().context("Invalid UTF-8")?;
160 let home = utils::dir::home_dir()?;
161 let home = home.to_str().context("Invalid UTF-8")?;
162
163 let user = users::get_user_by_uid(users::get_current_uid()).context("Cannot get user name")?;
164 let user = user.name().to_str().context("Invalid UTF-8 in user name")?;
165 let group =
166 users::get_group_by_gid(users::get_current_gid()).context("Cannot get group name")?;
167 let group = group
168 .name()
169 .to_str()
170 .context("Invalid UTF-8 in group name")?;
171
172 const TIMEOUT_STOP_SEC: u32 = 20;
173
174 let src = format!(
175 "\
176# Auto generated by shanghai
177# symbolic link from: /etc/systemd/system/
178# `systemctl daemon-reload`
179# `systemctl enable shanghai` to auto start on boot
180
181[Unit]
182Description=House Management System
183Wants = network-online.target
184After = network-online.target
185
186[Service]
187Type=simple
188Restart=always
189User={user}
190Group={group}
191Environment=HOME={home}
192WorkingDirectory={cd}
193ExecStart={exe}
194# ExecStop default: SYGTERM
195TimeoutStopSec={TIMEOUT_STOP_SEC}
196ExecReload=/bin/kill -s SIGUSR1 $MAINPID
197
198[Install]
199WantedBy=multi-user.target
200"
201 );
202
203 log::info!("systemd service: {}", file_path.to_string_lossy());
204 std::fs::create_dir_all(&dir)?;
205 std::fs::write(&file_path, src)?;
206
207 Ok(())
208}
209
210fn print_help(program: &str, opts: Options) {
215 let brief = format!("Usage: {program} [options]");
216 print!("{}", opts.usage(&brief));
217}
218
219pub fn main() -> Result<()> {
223 let args: Vec<String> = env::args().collect();
225 let program = &args[0];
226
227 let mut opts = Options::new();
228 opts.optflag("h", "help", "Print this help");
229 opts.optflag("v", "verbose", "Print verbose logs on stdout");
230 let matches = match opts.parse(&args[1..]) {
231 Ok(m) => m,
232 Err(fail) => {
233 eprintln!("{fail}");
234 std::process::exit(1);
235 }
236 };
237
238 if matches.opt_present("h") {
240 print_help(program, opts);
241 std::process::exit(0);
242 }
243
244 let verbose = matches.opt_present("v");
245
246 let _flush = init_log(verbose)?;
247 create_systemd_files()?;
248
249 system_main().map_err(|e| {
250 error!("Error in system_main");
251 error!("{e:#}");
252 e
253 })
254
255 }