zng_ext_single_instance/
lib.rs

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