Skip to main content

perforce_cli/cmd/
bgtask.rs

1use std::path::{Path, PathBuf};
2use std::process::{Child, Command, Stdio};
3
4use super::{ExclusiveOption, SubCommand, Unselected};
5
6use crate::global::GlobalOpts;
7use crate::spawn::ParameterizedSpawn;
8
9/// Upper bound (in seconds) of the `-i` interval: the number of seconds in
10/// 31 days.
11const MAX_INTERVAL_SECONDS: u64 = 2_678_400;
12
13// ---------------------------------------------------------------------------
14// Task-target dimension: `-e command` vs `-t triggerName`
15// ---------------------------------------------------------------------------
16
17/// The task is given as a command string on the command line (`-e command`).
18///
19/// Entered with [`BackGroundTask::execute`]; mutually exclusive with
20/// [`TriggerMode`].
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct CommandMode {
23    execute: String,
24}
25
26impl ExclusiveOption for CommandMode {
27    fn inject_args(&self, command: &mut Command) {
28        command.arg("-e").arg(&self.execute);
29    }
30}
31
32/// The task is a trigger already defined in the triggers table
33/// (`-t triggerName`).
34///
35/// Entered with [`BackGroundTask::trigger`]; mutually exclusive with
36/// [`CommandMode`].
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct TriggerMode {
39    trigger: String,
40}
41
42impl ExclusiveOption for TriggerMode {
43    fn inject_args(&self, command: &mut Command) {
44        command.arg("-t").arg(&self.trigger);
45    }
46}
47
48/// Marker for a selected task target: one of [`CommandMode`] or
49/// [`TriggerMode`].
50///
51/// Exactly one of `-e` / `-t` is required, so spawning is only available once
52/// [`BackGroundTask`] has transitioned into one of these states.
53pub trait TaskTarget: ExclusiveOption {}
54
55impl TaskTarget for CommandMode {}
56impl TaskTarget for TriggerMode {}
57
58// ---------------------------------------------------------------------------
59// Command struct
60// ---------------------------------------------------------------------------
61
62/// Type-safe builder for `p4 bgtask`.
63///
64/// Run background commands or triggers on the server. The server saves the
65/// task output to its log file; this command requires `super` access.
66///
67/// # Syntax
68///
69/// ```text
70/// p4 [g-opts] bgtask [-b retries] [-d] [-i interval] [-m runCount]
71///                    [-w seconds] {-e command | -t triggerName}
72/// ```
73///
74/// The `M` type parameter tracks the required task target at compile time:
75/// it starts as [`Unselected`], and one of [`Self::execute`] /
76/// [`Self::trigger`] must be used to move into the [`CommandMode`] /
77/// [`TriggerMode`] state before the command can be spawned. The two states
78/// can be switched freely with the same two methods.
79#[derive(Debug, Clone, Default)]
80pub struct BackGroundTask<M = Unselected> {
81    bin: PathBuf,
82
83    global_opts: GlobalOpts,
84
85    /// `-b retries`
86    retries: Option<u64>,
87
88    /// `-d`
89    detach: bool,
90
91    /// `-i interval`
92    interval: Option<u64>,
93
94    /// `-m runCount`
95    runs: Option<u64>,
96
97    /// `-w seconds`
98    wait: Option<u64>,
99
100    mode: M,
101}
102
103impl BackGroundTask<Unselected> {
104    /// Creates a new `p4 bgtask` command with no task target selected.
105    ///
106    /// `bin` is the path to the Perforce command-line executable. Use
107    /// [`Self::execute`] or [`Self::trigger`] to select the required
108    /// `-e` / `-t` target.
109    pub fn new(bin: impl Into<PathBuf>, global_opts: GlobalOpts) -> Self {
110        Self {
111            bin: bin.into(),
112            global_opts,
113            ..Default::default()
114        }
115    }
116}
117
118impl<M> BackGroundTask<M> {
119    /// Get the Perforce command path.
120    pub fn get_bin(&self) -> &Path {
121        &self.bin
122    }
123
124    /// Set the Perforce command path.
125    pub fn set_bin(&mut self, bin: impl Into<PathBuf>) -> &mut Self {
126        self.bin = bin.into();
127        self
128    }
129
130    /// Set the Perforce command path.
131    pub fn bin(mut self, bin: impl Into<PathBuf>) -> Self {
132        self.set_bin(bin);
133        self
134    }
135
136    /// Selects `-e command`: run the given command string on the server.
137    ///
138    /// This transitions into the [`CommandMode`] state, discarding any
139    /// previously selected `-e` / `-t` target; the scheduling options are
140    /// preserved.
141    pub fn execute(self, command: impl Into<String>) -> BackGroundTask<CommandMode> {
142        BackGroundTask {
143            bin: self.bin,
144            global_opts: self.global_opts,
145            retries: self.retries,
146            detach: self.detach,
147            interval: self.interval,
148            runs: self.runs,
149            wait: self.wait,
150            mode: CommandMode {
151                execute: command.into(),
152            },
153        }
154    }
155
156    /// Selects `-t triggerName`: run the named bgtask trigger, already defined
157    /// in the triggers table, on the server.
158    ///
159    /// This transitions into the [`TriggerMode`] state, discarding any
160    /// previously selected `-e` / `-t` target; the scheduling options are
161    /// preserved.
162    pub fn trigger(self, name: impl Into<String>) -> BackGroundTask<TriggerMode> {
163        BackGroundTask {
164            bin: self.bin,
165            global_opts: self.global_opts,
166            retries: self.retries,
167            detach: self.detach,
168            interval: self.interval,
169            runs: self.runs,
170            wait: self.wait,
171            mode: TriggerMode {
172                trigger: name.into(),
173            },
174        }
175    }
176
177    /// # Description
178    ///
179    /// g-opts
180    ///
181    #[cfg_attr(
182        all(feature = "lt2018_2", not(feature = "lt2017_1")),
183        doc = "See [Global Options](GlobalOpts)."
184    )]
185    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
186    pub fn get_global_opts(&self) -> &GlobalOpts {
187        &self.global_opts
188    }
189
190    /// # Description
191    ///
192    /// g-opts
193    ///
194    #[cfg_attr(
195        all(feature = "lt2018_2", not(feature = "lt2017_1")),
196        doc = "See [Global Options](GlobalOpts)."
197    )]
198    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
199    pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
200        self.global_opts = v;
201        self
202    }
203
204    /// # Description
205    ///
206    /// g-opts
207    ///
208    #[cfg_attr(
209        all(feature = "lt2018_2", not(feature = "lt2017_1")),
210        doc = "See [Global Options](GlobalOpts)."
211    )]
212    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
213    pub fn global_opts(mut self, v: GlobalOpts) -> Self {
214        self.global_opts = v;
215        self
216    }
217
218    /// # Description
219    ///
220    /// -b retries
221    ///
222    /// Maximum number of execution errors before ceasing to attempt
223    /// execution. The default is `1`.
224    pub fn get_retries(&self) -> Option<u64> {
225        self.retries
226    }
227
228    /// # Description
229    ///
230    /// -b retries
231    ///
232    /// Maximum number of execution errors before ceasing to attempt
233    /// execution. The default is `1`.
234    pub fn set_retries(&mut self, v: u64) -> &mut Self {
235        self.retries = Some(v);
236        self
237    }
238
239    /// # Description
240    ///
241    /// -b retries
242    ///
243    /// Maximum number of execution errors before ceasing to attempt
244    /// execution. The default is `1`.
245    pub fn retries(mut self, v: u64) -> Self {
246        self.retries = Some(v);
247        self
248    }
249
250    /// # Description
251    ///
252    /// -d
253    ///
254    /// Detach the client so it does not see the output of the server-side
255    /// task execution. Not allowed when the task is specified in a
256    /// `startup.N` configurable.
257    pub fn get_detach(&self) -> bool {
258        self.detach
259    }
260
261    /// # Description
262    ///
263    /// -d
264    ///
265    /// Detach the client so it does not see the output of the server-side
266    /// task execution. Not allowed when the task is specified in a
267    /// `startup.N` configurable.
268    pub fn set_detach(&mut self, v: bool) -> &mut Self {
269        self.detach = v;
270        self
271    }
272
273    /// # Description
274    ///
275    /// -d
276    ///
277    /// Detach the client so it does not see the output of the server-side
278    /// task execution. Not allowed when the task is specified in a
279    /// `startup.N` configurable.
280    pub fn detach(mut self, v: bool) -> Self {
281        self.detach = v;
282        self
283    }
284
285    /// # Description
286    ///
287    /// -i interval
288    ///
289    /// Seconds between command invocations. The default is `1` second; the
290    /// maximum is 2,678,400 (the number of seconds in 31 days).
291    pub fn get_interval(&self) -> Option<u64> {
292        self.interval
293    }
294
295    /// # Description
296    ///
297    /// -i interval
298    ///
299    /// Seconds between command invocations. The default is `1` second; the
300    /// maximum is 2,678,400 (the number of seconds in 31 days).
301    pub fn set_interval(&mut self, v: u64) -> &mut Self {
302        debug_assert!(
303            v <= MAX_INTERVAL_SECONDS,
304            "bgtask -i interval must not exceed {MAX_INTERVAL_SECONDS} seconds (31 days)"
305        );
306        self.interval = Some(v);
307        self
308    }
309
310    /// # Description
311    ///
312    /// -i interval
313    ///
314    /// Seconds between command invocations. The default is `1` second; the
315    /// maximum is 2,678,400 (the number of seconds in 31 days).
316    pub fn interval(mut self, v: u64) -> Self {
317        debug_assert!(
318            v <= MAX_INTERVAL_SECONDS,
319            "bgtask -i interval must not exceed {MAX_INTERVAL_SECONDS} seconds (31 days)"
320        );
321        self.interval = Some(v);
322        self
323    }
324
325    /// # Description
326    ///
327    /// -m runCount
328    ///
329    /// Maximum number of times the command is run. The default is `1`.
330    pub fn get_runs(&self) -> Option<u64> {
331        self.runs
332    }
333
334    /// # Description
335    ///
336    /// -m runCount
337    ///
338    /// Maximum number of times the command is run. The default is `1`.
339    pub fn set_runs(&mut self, v: u64) -> &mut Self {
340        self.runs = Some(v);
341        self
342    }
343
344    /// # Description
345    ///
346    /// -m runCount
347    ///
348    /// Maximum number of times the command is run. The default is `1`.
349    pub fn runs(mut self, v: u64) -> Self {
350        self.runs = Some(v);
351        self
352    }
353
354    /// # Description
355    ///
356    /// -w seconds
357    ///
358    /// Seconds to wait after an execution error before attempting the next
359    /// execution. The default is `5`.
360    pub fn get_wait(&self) -> Option<u64> {
361        self.wait
362    }
363
364    /// # Description
365    ///
366    /// -w seconds
367    ///
368    /// Seconds to wait after an execution error before attempting the next
369    /// execution. The default is `5`.
370    pub fn set_wait(&mut self, v: u64) -> &mut Self {
371        self.wait = Some(v);
372        self
373    }
374
375    /// # Description
376    ///
377    /// -w seconds
378    ///
379    /// Seconds to wait after an execution error before attempting the next
380    /// execution. The default is `5`.
381    pub fn wait(mut self, v: u64) -> Self {
382        self.wait = Some(v);
383        self
384    }
385}
386
387impl BackGroundTask<CommandMode> {
388    /// The command string selected with `-e`.
389    pub fn get_command(&self) -> &str {
390        &self.mode.execute
391    }
392}
393
394impl BackGroundTask<TriggerMode> {
395    /// The trigger name selected with `-t`.
396    pub fn get_trigger(&self) -> &str {
397        &self.mode.trigger
398    }
399}
400
401// ---------------------------------------------------------------------------
402// SubCommand
403// ---------------------------------------------------------------------------
404
405impl<M: ExclusiveOption> SubCommand for BackGroundTask<M> {
406    fn name(&self) -> &str {
407        "bgtask"
408    }
409
410    fn inject_local_args(&self, command: &mut Command) {
411        if let Some(retries) = self.retries {
412            command.arg("-b").arg(retries.to_string());
413        }
414
415        if self.detach {
416            command.arg("-d");
417        }
418
419        if let Some(interval) = self.interval {
420            command.arg("-i").arg(interval.to_string());
421        }
422
423        if let Some(runs) = self.runs {
424            command.arg("-m").arg(runs.to_string());
425        }
426
427        if let Some(wait) = self.wait {
428            command.arg("-w").arg(wait.to_string());
429        }
430
431        self.mode.inject_args(command);
432    }
433
434    fn global_opts(&self) -> Option<&GlobalOpts> {
435        Some(&self.global_opts)
436    }
437}
438
439// ---------------------------------------------------------------------------
440// Spawning: only after a task target (`-e` / `-t`) has been selected
441// ---------------------------------------------------------------------------
442
443impl<M: TaskTarget> ParameterizedSpawn<()> for BackGroundTask<M> {
444    type Output = Child;
445    type Error = std::io::Error;
446
447    /// Spawns `p4 bgtask` as a child process with piped standard output and
448    /// error streams; use the returned [`Child`] handle to wait for it or
449    /// interact with it.
450    fn spawn_with(&mut self, (): ()) -> Result<Self::Output, Self::Error> {
451        self.setup_command(&self.bin)
452            .stdout(Stdio::piped())
453            .stderr(Stdio::piped())
454            .spawn()
455    }
456}
457
458#[cfg(test)]
459mod tests {
460    use super::*;
461    use crate::cmd::args_of;
462
463    #[test]
464    fn bin_accessors_update_executable_path() {
465        let mut command = BackGroundTask::new("p4", GlobalOpts::new()).trigger("verify");
466        assert_eq!(command.get_bin(), Path::new("p4"));
467
468        command.set_bin("/usr/local/bin/p4");
469        assert_eq!(command.get_bin(), Path::new("/usr/local/bin/p4"));
470
471        let command = command.bin("p4.exe");
472        assert_eq!(command.get_bin(), Path::new("p4.exe"));
473    }
474
475    /// Dry-run checks of the assembled `p4 bgtask` command line; no process
476    /// is spawned.
477    #[test]
478    fn execute_target_only() {
479        let task = BackGroundTask::new("p4", GlobalOpts::new()).execute("top -b -n 1");
480
481        assert_eq!(
482            args_of(&task.setup_command("p4")),
483            ["bgtask", "-e", "top -b -n 1"]
484        );
485        assert_eq!(task.get_command(), "top -b -n 1");
486    }
487
488    #[test]
489    fn trigger_target_only() {
490        let task = BackGroundTask::new("p4", GlobalOpts::new()).trigger("log_checker");
491
492        assert_eq!(
493            args_of(&task.setup_command("p4")),
494            ["bgtask", "-t", "log_checker"]
495        );
496        assert_eq!(task.get_trigger(), "log_checker");
497    }
498
499    #[test]
500    fn all_options_injected_in_syntax_order() {
501        let task = BackGroundTask::new("p4", GlobalOpts::new())
502            .retries(3)
503            .detach(true)
504            .interval(86_400)
505            .runs(5)
506            .wait(10)
507            .trigger("p4dstate");
508
509        assert_eq!(
510            args_of(&task.setup_command("p4")),
511            [
512                "bgtask", "-b", "3", "-d", "-i", "86400", "-m", "5", "-w", "10", "-t", "p4dstate"
513            ]
514        );
515    }
516
517    #[test]
518    fn global_opts_are_injected_once() {
519        let task =
520            BackGroundTask::new("p4", GlobalOpts::new().port("localhost:1666")).execute("verify");
521
522        assert_eq!(
523            args_of(&task.setup_command("p4")),
524            ["-p", "localhost:1666", "bgtask", "-e", "verify"]
525        );
526    }
527
528    #[test]
529    fn switching_targets_replaces_selection() {
530        let task = BackGroundTask::new("p4", GlobalOpts::new())
531            .runs(2)
532            .execute("old command")
533            .trigger("verify");
534
535        assert_eq!(
536            args_of(&task.setup_command("p4")),
537            ["bgtask", "-m", "2", "-t", "verify"]
538        );
539        assert_eq!(task.get_trigger(), "verify");
540
541        let task = task.execute("new command");
542        assert_eq!(
543            args_of(&task.setup_command("p4")),
544            ["bgtask", "-m", "2", "-e", "new command"]
545        );
546        assert_eq!(task.get_command(), "new command");
547    }
548
549    #[test]
550    fn interval_accepts_the_document_maximum() {
551        let task = BackGroundTask::new("p4", GlobalOpts::new())
552            .interval(MAX_INTERVAL_SECONDS)
553            .trigger("verify");
554
555        assert_eq!(
556            args_of(&task.setup_command("p4")),
557            ["bgtask", "-i", "2678400", "-t", "verify"]
558        );
559    }
560
561    #[test]
562    #[should_panic(expected = "31 days")]
563    fn interval_above_maximum_panics_in_debug() {
564        let _ = BackGroundTask::new("p4", GlobalOpts::new()).interval(MAX_INTERVAL_SECONDS + 1);
565    }
566
567    #[test]
568    fn setters_update_the_same_command() {
569        let mut task = BackGroundTask::new("p4", GlobalOpts::new()).trigger("verify");
570        task.set_retries(4).set_detach(true).set_wait(30);
571
572        assert_eq!(task.get_retries(), Some(4));
573        assert!(task.get_detach());
574        assert_eq!(task.get_wait(), Some(30));
575        assert_eq!(
576            args_of(&task.setup_command("p4")),
577            ["bgtask", "-b", "4", "-d", "-w", "30", "-t", "verify"]
578        );
579    }
580}