shanghai/
main.rs

1//! Rust 版管理人形。
2//!
3//! 設定ファイルの説明は [sys::config::Config] にある。
4
5// ドキュメントはライブラリの外部仕様の説明のためではなく、
6// private も含めた実装の解説のために生成する。
7#![allow(rustdoc::private_intra_doc_links)]
8
9use anyhow::Result;
10use customlog::{ConsoleLogger, FileLogger, FlushGuard, RotateOptions, RotateSize};
11use daemonize::Daemonize;
12use getopts::Options;
13use log::{LevelFilter, Log, error, info};
14use std::env;
15use std::fs::File;
16use std::io::{BufWriter, Write};
17use std::os::unix::prelude::*;
18use sys::sysmod::SystemModules;
19use sys::taskserver::{Control, RunResult, TaskServer};
20
21/// デーモン化の際に指定する stdout のリダイレクト先。
22const FILE_STDOUT: &str = "stdout.txt";
23/// デーモン化の際に指定する stderr のリダイレクト先。
24const FILE_STDERR: &str = "stderr.txt";
25/// デーモン用シェルスクリプトの出力先。
26const FILE_EXEC_SH: &str = "exec.sh";
27/// デーモン用シェルスクリプトの出力先。
28const FILE_KILL_SH: &str = "kill.sh";
29/// デーモン用シェルスクリプトの出力先。
30const FILE_FLUSH_SH: &str = "flushlog.sh";
31/// Cron 設定例の出力先。
32const FILE_CRON: &str = "cron.txt";
33/// デーモン化の際に指定する pid ファイルパス。
34const FILE_PID: &str = "shanghai.pid";
35/// ログのファイル出力先。
36const FILE_LOG: &str = "shanghai.log";
37
38const LOG_FILTER: &[&str] = &[module_path!(), "sys"];
39const LOG_ROTATE_SIZE: usize = 1024 * 1024;
40const LOG_ROTATE_COUNT: u16 = 10;
41const LOG_BUF_SIZE: usize = 64 * 1024;
42
43/// stdout, stderr をリダイレクトし、デーモン化する。
44///
45/// ファイルオープンに失敗したら exit(1) する。
46/// デーモン化に失敗したら exit(1) する。
47/// 成功した場合は元の親プロセスは正常終了し、子プロセスが以降の処理を続行する。
48fn daemon() {
49    let stdout = match File::create(FILE_STDOUT) {
50        Ok(f) => f,
51        Err(e) => {
52            eprintln!("Open {FILE_STDOUT} error: {e}");
53            std::process::exit(1);
54        }
55    };
56    let stderr = match File::create(FILE_STDERR) {
57        Ok(f) => f,
58        Err(e) => {
59            eprintln!("Open {FILE_STDERR} error: {e}");
60            std::process::exit(1);
61        }
62    };
63
64    let daemonize = Daemonize::new()
65        .pid_file(FILE_PID)
66        //.chown_pid_file(true)
67        .working_directory(".")
68        //.user("nobody")
69        //.group("daemon")
70        .stdout(stdout)
71        .stderr(stderr);
72
73    if let Err(e) = daemonize.start() {
74        eprintln!("Daemonize error: {e}");
75        std::process::exit(1);
76    }
77}
78
79fn log_target_filter(target: &str) -> bool {
80    LOG_FILTER.iter().any(|filter| target.starts_with(filter))
81}
82
83/// ロギングシステムを有効化する。
84///
85/// デーモンならファイルへの書き出しのみ、
86/// そうでないならファイルと stdout へ書き出す。
87///
88/// ログレベルは Error, Warn, Info, Debug, Trace の5段階である。
89/// ファイルへは Info 以上、stdout へは Trace 以上のログが出力される。
90///
91/// * `is_daemon` - デーモンかどうか。
92fn init_log(is_daemon: bool) -> FlushGuard {
93    // filter = Off, Error, Warn, Info, Debug, Trace
94    let rotate_opts = RotateOptions {
95        size: RotateSize::Enabled(LOG_ROTATE_SIZE),
96        file_count: LOG_ROTATE_COUNT,
97        ..Default::default()
98    };
99    let file_log = FileLogger::new_boxed(
100        LevelFilter::Info,
101        log_target_filter,
102        customlog::default_formatter,
103        FILE_LOG,
104        LOG_BUF_SIZE,
105        rotate_opts,
106    )
107    .expect("Log file open failed");
108
109    let loggers: Vec<Box<dyn Log>> = if is_daemon {
110        vec![file_log]
111    } else {
112        let console_log = ConsoleLogger::new_boxed(
113            customlog::Console::Stdout,
114            LevelFilter::Trace,
115            log_target_filter,
116            customlog::default_formatter,
117        );
118        vec![console_log, file_log]
119    };
120    customlog::init(loggers, LevelFilter::Trace)
121}
122
123/// 起動時に一度だけブートメッセージをツイートするタスク。
124async fn boot_msg_task(ctrl: Control) -> Result<()> {
125    let build_info = verinfo::version_info();
126    // 同一テキストをツイートしようとするとエラーになるので日時を含める
127    let now = chrono::Local::now();
128    let now = now.format("%F %T %:z");
129    let msg = format!("[{now}] Boot...\n{build_info}");
130
131    {
132        let mut twitter = ctrl.sysmods().twitter.lock().await;
133        if let Err(why) = twitter.tweet(&msg).await {
134            error!("error on tweet");
135            error!("{why:#?}");
136        }
137    }
138    {
139        let mut discord = ctrl.sysmods().discord.lock().await;
140        if let Err(why) = discord.say(&msg).await {
141            error!("error on discord notification");
142            error!("{why:#?}");
143        }
144    }
145
146    Ok(())
147}
148
149/// システムメイン処理。
150/// コマンドラインとデーモン化、ログの初期化の後に入る。
151///
152/// 設定データをロードする。
153/// その後、システムモジュールとタスクサーバを初期化し、システムの実行を開始する。
154///
155/// * SIGUSR1: ログのフラッシュ
156/// * SIGUSR2: なし
157fn system_main() -> Result<()> {
158    let sigusr1 = || {
159        info!("Flush log");
160        log::logger().flush();
161        None
162    };
163    let sigusr2 = || None;
164
165    loop {
166        info!("system main");
167        info!("{}", verinfo::version_info());
168        log::logger().flush();
169
170        sys::config::load()?;
171
172        let sysmods = SystemModules::new()?;
173        let ts = TaskServer::new(sysmods);
174
175        ts.spawn_oneshot_task("boot_msg", boot_msg_task);
176        let run_result = ts.run(sigusr1, sigusr2);
177
178        info!("task server dropped");
179
180        match run_result {
181            RunResult::Shutdown => {
182                info!("result: shutdown");
183                break;
184            }
185            RunResult::Reboot => {
186                info!("result: reboot");
187            }
188        }
189    }
190
191    Ok(())
192}
193
194/// 実行可能パーミッション 755 でファイルを作成して close せずに返す。
195fn create_sh(path: &str) -> Result<File> {
196    let f = File::create(path)?;
197
198    let mut perm = f.metadata()?.permissions();
199    perm.set_mode(0o755);
200    f.set_permissions(perm)?;
201
202    Ok(f)
203}
204
205/// 実行ファイル絶対パスから便利なスクリプトを生成する。
206///
207/// [FILE_EXEC_SH], [FILE_KILL_SH], [FILE_CRON].
208fn create_run_script() -> Result<()> {
209    let exe = env::current_exe()?.to_string_lossy().to_string();
210    let cd = env::current_dir()?.to_string_lossy().to_string();
211
212    {
213        let f = create_sh(FILE_EXEC_SH)?;
214        let mut w = BufWriter::new(f);
215
216        writeln!(&mut w, "#!/bin/bash")?;
217        writeln!(&mut w, "set -euxo pipefail")?;
218        writeln!(&mut w)?;
219        writeln!(&mut w, "cd '{cd}'")?;
220        writeln!(&mut w, "'{exe}' --daemon")?;
221    }
222    {
223        let f = create_sh(FILE_KILL_SH)?;
224        let mut w = BufWriter::new(f);
225
226        writeln!(&mut w, "#!/bin/bash")?;
227        writeln!(&mut w, "set -euxo pipefail")?;
228        writeln!(&mut w)?;
229        writeln!(&mut w, "cd '{cd}'")?;
230        writeln!(&mut w, "kill `cat {FILE_PID}`")?;
231    }
232    {
233        let f = create_sh(FILE_FLUSH_SH)?;
234        let mut w = BufWriter::new(f);
235
236        writeln!(&mut w, "#!/bin/bash")?;
237        writeln!(&mut w, "set -euxo pipefail")?;
238        writeln!(&mut w)?;
239        writeln!(&mut w, "cd '{cd}'")?;
240        writeln!(&mut w, "kill -SIGUSR1 `cat {FILE_PID}`")?;
241    }
242    {
243        let f = File::create(FILE_CRON)?;
244        let mut w = BufWriter::new(f);
245
246        write!(
247            &mut w,
248            "# How to use
249# $ crontab < {FILE_CRON}
250# How to verify
251# $ crontab -l
252
253# workaround: wait for 30 sec to wait for network
254# It seems that DNS fails just after reboot
255
256@reboot sleep 30; cd {cd}; ./{FILE_EXEC_SH}
257"
258        )?;
259    }
260
261    Ok(())
262}
263
264/// コマンドラインのヘルプを表示する。
265///
266/// * `program` - プログラム名 (argv\[0\])。
267/// * `opts` - パーサオブジェクト。
268fn print_help(program: &str, opts: Options) {
269    let brief = format!("Usage: {program} [options]");
270    print!("{}", opts.usage(&brief));
271}
272
273/// エントリポイント。
274///
275/// コマンドラインとデーモン化、ログの初期化処理をしたのち、[system_main] を呼ぶ。
276pub fn main() -> Result<()> {
277    create_run_script()?;
278
279    // コマンドライン引数のパース
280    let args: Vec<String> = env::args().collect();
281    let program = &args[0];
282
283    let mut opts = Options::new();
284    opts.optflag("h", "help", "Print this help");
285    opts.optflag("d", "daemon", "Run as daemon");
286    let matches = match opts.parse(&args[1..]) {
287        Ok(m) => m,
288        Err(fail) => {
289            eprintln!("{fail}");
290            std::process::exit(1);
291        }
292    };
293
294    // --help がある場合は出力して終了
295    if matches.opt_present("h") {
296        print_help(program, opts);
297        std::process::exit(0);
298    }
299
300    let _flush = if matches.opt_present("d") {
301        daemon();
302        init_log(true)
303    } else {
304        init_log(false)
305    };
306
307    system_main().map_err(|e| {
308        error!("Error in system_main");
309        error!("{e:#}");
310        e
311    })
312
313    // drop(_flush)
314}