zng_ext_single_instance/
lib.rs

1#![doc(html_favicon_url = "https://zng-ui.github.io/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://zng-ui.github.io/res/zng-logo.png")]
3//!
4//! Single app-process instance mode.
5//!
6//! # Crate
7//!
8#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
9
10use std::{
11    io::{Read, Write},
12    time::Duration,
13};
14
15use zng_app::{
16    AppExtension,
17    event::{event, event_args},
18    handler::{async_hn, clmv},
19};
20use zng_ext_fs_watcher::WATCHER;
21use zng_txt::{ToTxt, Txt};
22
23/// Single instance event manager.
24///
25/// # Events
26///
27/// Events this extension provides.
28///
29/// * [`APP_INSTANCE_EVENT`]
30#[derive(Default)]
31#[non_exhaustive]
32pub struct SingleInstanceManager {}
33impl AppExtension for SingleInstanceManager {
34    fn init(&mut self) {
35        let args: Box<[_]> = std::env::args().map(Txt::from).collect();
36        APP_INSTANCE_EVENT.notify(AppInstanceArgs::now(args, 0usize));
37
38        let name = match SINGLE_INSTANCE.lock().as_ref().map(|l| l.name.clone()) {
39            Some(n) => n,
40            None => return, // app is running in a special process, like a crash dialog
41        };
42
43        let args_file = std::env::temp_dir().join(name);
44        let mut count = 1usize;
45        WATCHER
46            .on_file_changed(
47                &args_file,
48                async_hn!(args_file, |_| {
49                    let args = zng_task::wait(clmv!(args_file, || {
50                        for i in 0..5 {
51                            if i > 0 {
52                                std::thread::sleep(Duration::from_millis(200));
53                            }
54
55                            // take args
56                            // read all text and truncates the file
57                            match std::fs::File::options().read(true).write(true).open(&args_file) {
58                                Ok(mut file) => {
59                                    let mut s = String::new();
60                                    if let Err(e) = file.read_to_string(&mut s) {
61                                        tracing::error!("error reading args (retry {i}), {e}");
62                                        continue;
63                                    }
64                                    file.set_len(0).unwrap();
65                                    return s;
66                                }
67                                Err(e) => {
68                                    if e.kind() == std::io::ErrorKind::NotFound {
69                                        return String::new();
70                                    }
71                                    tracing::error!("error reading args (retry {i}), {e}")
72                                }
73                            }
74                        }
75                        String::new()
76                    }))
77                    .await;
78
79                    // parse args
80                    for line in args.lines() {
81                        let line = line.trim();
82                        if line.is_empty() {
83                            continue;
84                        }
85
86                        let args = match serde_json::from_str::<Box<[Txt]>>(line) {
87                            Ok(args) => args,
88                            Err(e) => {
89                                tracing::error!("invalid args, {e}");
90                                Box::new([])
91                            }
92                        };
93
94                        APP_INSTANCE_EVENT.notify(AppInstanceArgs::now(args, count));
95
96                        count += 1;
97                    }
98                }),
99            )
100            .perm();
101    }
102}
103
104event_args! {
105    /// Arguments for [`APP_INSTANCE_EVENT`].
106    pub struct AppInstanceArgs {
107        /// Arguments the app instance was started with.
108        ///
109        /// See [`std::env::args`] for more details.
110        pub args: Box<[Txt]>,
111
112        /// Instance count. Is zero for the current process, in single instance mode
113        /// increments for each subsequent attempt to instantiate the app.
114        pub count: usize,
115
116        ..
117
118        fn delivery_list(&self, _list: &mut UpdateDeliveryList) {}
119    }
120}
121impl AppInstanceArgs {
122    /// If the arguments are for the currently executing process (main).
123    ///
124    /// This is only `true` once, on the first event on startup.
125    pub fn is_current(&self) -> bool {
126        self.count == 0
127    }
128}
129
130event! {
131    /// App instance init event, with the arguments.
132    ///
133    /// This event notifies once on start. If the app is "single instance" this event will also notify for each
134    /// new attempt to instantiate while the current process is already running.
135    pub static APP_INSTANCE_EVENT: AppInstanceArgs;
136}
137
138zng_env::on_process_start!(|args| {
139    if args.next_handlers_count > 0 && args.yield_count < zng_env::ProcessStartArgs::MAX_YIELD_COUNT {
140        // absolute sure that this is the app-process
141        return args.yield_once();
142    }
143
144    let mut lock = SINGLE_INSTANCE.lock();
145    assert!(lock.is_none(), "single_instance already called in this process");
146
147    let name = std::env::current_exe()
148        .and_then(dunce::canonicalize)
149        .expect("current exe is required")
150        .display()
151        .to_txt();
152    let name: String = name
153        .chars()
154        .map(|c| if c.is_ascii_alphanumeric() || c == '-' { c } else { '_' })
155        .collect();
156    let mut name = name.as_str();
157    if name.len() > 128 {
158        name = &name[name.len() - 128..];
159    }
160    let name = zng_txt::formatx!("zng-si-{name}");
161
162    let l = single_instance::SingleInstance::new(&name).expect("failed to create single instance lock");
163
164    if l.is_single() {
165        *lock = Some(SingleInstanceData { _lock: l, name });
166    } else {
167        tracing::info!("another instance running, will send args and exit");
168
169        let args: Box<[_]> = std::env::args().collect();
170        let args = format!("\n{}\n", serde_json::to_string(&args).unwrap());
171
172        let try_write = move || -> std::io::Result<()> {
173            let mut file = std::fs::File::options()
174                .create(true)
175                .append(true)
176                .open(std::env::temp_dir().join(name.as_str()))?;
177            file.write_all(args.as_bytes())
178        };
179
180        for i in 0..5 {
181            if i > 0 {
182                std::thread::sleep(std::time::Duration::from_millis(300));
183            }
184            match try_write() {
185                Ok(_) => zng_env::exit(0),
186                Err(e) => {
187                    eprintln!("error writing args (retries: {i}), {e}");
188                }
189            }
190        }
191        zng_env::exit(1);
192    }
193});
194
195struct SingleInstanceData {
196    _lock: single_instance::SingleInstance,
197    name: Txt,
198}
199
200static SINGLE_INSTANCE: parking_lot::Mutex<Option<SingleInstanceData>> = parking_lot::Mutex::new(None);