shanghai/
main.rs

1//! Rust 版管理人形。
2//!
3//! 設定ファイルの説明は [sys::config::Config] にある。
4
5// ドキュメントはライブラリの外部仕様の説明のためではなく、
6// private も含めた実装の解説のために生成する。
7#![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
17/// ログのファイル出力先。
18const 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
30/// ロギングシステムを有効化する。
31///
32/// 出力先は stdout と ファイル。
33/// ログレベルは Error, Warn, Info, Debug, Trace の5段階である。
34/// フィルタは Info 以上、
35/// ただし verbose モードの場合は stdout へは Trace 以上のログが出力される。
36/// (debug build では自動的に)
37///
38/// * `opts` - 起動オプション。
39fn init_log(verbose: bool) -> Result<FlushGuard> {
40    // filter = Off, Error, Warn, Info, Debug, Trace
41    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    // -v または debug build なら最大出力にする
60    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
79/// 起動時に一度だけブートメッセージをツイートするタスク。
80async fn boot_msg_task(ctrl: Control) -> Result<()> {
81    let build_info = verinfo::version_info();
82    // 同一テキストをツイートしようとするとエラーになるので日時を含める
83    let now = chrono::Local::now();
84    let now = now.format("%F %T %:z");
85    let msg = format!("[{now}] Boot...\n{build_info}");
86
87    /*{
88        let mut twitter = ctrl.sysmods().twitter.lock().await;
89        if let Err(why) = twitter.tweet(&msg).await {
90            error!("error on tweet");
91            error!("{why:#?}");
92        }
93    }*/
94    {
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
105/// システムメイン処理。
106/// コマンドラインとデーモン化、ログの初期化の後に入る。
107///
108/// 設定データをロードする。
109/// その後、システムモジュールとタスクサーバを初期化し、システムの実行を開始する。
110///
111/// * SIGUSR1: ログのフラッシュ
112/// * SIGUSR2: なし
113fn 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
210/// コマンドラインのヘルプを表示する。
211///
212/// * `program` - プログラム名 (argv\[0\])。
213/// * `opts` - パーサオブジェクト。
214fn print_help(program: &str, opts: Options) {
215    let brief = format!("Usage: {program} [options]");
216    print!("{}", opts.usage(&brief));
217}
218
219/// エントリポイント。
220///
221/// コマンドラインとデーモン化、ログの初期化処理をしたのち、[system_main] を呼ぶ。
222pub fn main() -> Result<()> {
223    // コマンドライン引数のパース
224    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    // --help がある場合は出力して終了
239    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    // drop(_flush)
256}