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