Skip to main content

yash_env/input/
eof_guard.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2026 WATANABE Yuki
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Defines the [`EofGuard`] input decorator, [`SuspendedJobsGuardConfig`], and
18//! [`IgnoreEofConfig`].
19
20use super::{Context, Input, Result};
21use crate::Env;
22use crate::io::Fd;
23use crate::option::{IgnoreEof as IgnoreEofOption, Interactive, Off, On, PosixlyCorrect};
24use crate::system::Isatty;
25use crate::system::concurrency::WriteAll;
26use std::cell::RefCell;
27
28/// Configuration for the suspended-jobs exit guard.
29///
30/// When present in [`Env::any`](crate::Env::any), [`EofGuard`] refuses to exit
31/// when there are suspended jobs, printing [`message`](Self::message) to warn
32/// the user. Other components may also opt in to this protection by checking
33/// for this config.
34///
35/// Store this config in the environment with
36/// `env.any.insert(Box::new(config))`.
37#[derive(Clone, Debug, Default)]
38#[non_exhaustive]
39pub struct SuspendedJobsGuardConfig {
40    /// Text displayed when exit is prevented because there are suspended jobs
41    pub message: String,
42}
43
44impl SuspendedJobsGuardConfig {
45    /// Creates a new `SuspendedJobsGuardConfig` with the given message.
46    #[must_use]
47    pub fn with_message<M: Into<String>>(message: M) -> Self {
48        Self {
49            message: message.into(),
50        }
51    }
52}
53
54/// Configuration for the [`EofGuard`]'s `ignore-eof` behavior.
55///
56/// When present in [`Env::any`](crate::Env::any), [`EofGuard`] retries reading
57/// on EOF when the [`ignore-eof` option](crate::option::IgnoreEof) is enabled,
58/// printing [`message`](Self::message) to remind the user.
59///
60/// If absent from `env.any`, the `ignore-eof` EOF protection in [`EofGuard`]
61/// is disabled.
62///
63/// Store this config in the environment with
64/// `env.any.insert(Box::new(config))`.
65///
66/// Note that [`IgnoreEof`](crate::input::IgnoreEof) is not affected by this config.
67#[derive(Clone, Debug, Default)]
68#[non_exhaustive]
69pub struct IgnoreEofConfig {
70    /// Text displayed when EOF is ignored due to the `ignore-eof` option
71    pub message: String,
72}
73
74impl IgnoreEofConfig {
75    /// Creates a new `IgnoreEofConfig` with the given message.
76    #[must_use]
77    pub fn with_message<M: Into<String>>(message: M) -> Self {
78        Self {
79            message: message.into(),
80        }
81    }
82}
83
84/// `Input` decorator that prevents premature exit on EOF
85///
86/// This is a decorator of [`Input`] that combines the behavior of the
87/// [`ignore-eof` shell option](crate::option::IgnoreEof) with protection
88/// against accidental exit when there are suspended jobs.
89///
90/// On EOF (empty line), the decorator retries reading if any of the following
91/// conditions is met (provided the shell is interactive, the input is a
92/// terminal, and the retry limit has not been reached):
93///
94/// 1. [`SuspendedJobsGuardConfig`] is present in `env.any`, the
95///    [`PosixlyCorrect`] option is off, and there are suspended jobs —
96///    prints [`SuspendedJobsGuardConfig::message`].
97/// 2. The `ignore-eof` option is enabled and [`IgnoreEofConfig`] is present in
98///    `env.any` — prints [`IgnoreEofConfig::message`].
99///
100/// The retry limit is 50 consecutive EOFs per [`next_line`](Input::next_line)
101/// call. Once the limit is reached the empty string is returned, allowing the
102/// shell to exit.
103///
104/// If neither [`SuspendedJobsGuardConfig`] nor [`IgnoreEofConfig`] is present
105/// in `env.any`, the decorator passes through all input unchanged (no retries).
106///
107/// Unlike [`IgnoreEof`](crate::input::IgnoreEof), this decorator also checks
108/// for suspended jobs, so it should be used instead of `IgnoreEof` in the
109/// top-level interactive read loop.
110#[derive(Debug)]
111pub struct EofGuard<'a, 'b, S, T> {
112    /// Inner input to read from
113    inner: T,
114    /// File descriptor to be checked if it is a terminal
115    fd: Fd,
116    /// Environment to check the shell options, jobs, and system interface
117    env: &'a RefCell<&'b mut Env<S>>,
118}
119
120impl<'a, 'b, S, T> EofGuard<'a, 'b, S, T> {
121    /// Creates a new `EofGuard` decorator.
122    ///
123    /// The arguments match those of [`IgnoreEof::new`](crate::input::IgnoreEof::new)
124    /// except that there is no `message` argument — the messages are read from
125    /// [`SuspendedJobsGuardConfig`] and [`IgnoreEofConfig`] stored in `env.any`.
126    ///
127    /// `inner` is the wrapped input, `fd` is the terminal file descriptor to
128    /// check, and `env` is the shared environment.
129    pub fn new(inner: T, fd: Fd, env: &'a RefCell<&'b mut Env<S>>) -> Self {
130        Self { inner, fd, env }
131    }
132}
133
134// Not derived automatically because S may not implement Clone.
135impl<S, T: Clone> Clone for EofGuard<'_, '_, S, T> {
136    fn clone(&self) -> Self {
137        Self {
138            inner: self.inner.clone(),
139            fd: self.fd,
140            env: self.env,
141        }
142    }
143}
144
145impl<S: Isatty + WriteAll, T: Input> Input for EofGuard<'_, '_, S, T> {
146    #[allow(
147        clippy::await_holding_refcell_ref,
148        reason = "other decorators, the parser, or the executor do not run concurrently with this method"
149    )]
150    async fn next_line(&mut self, context: &Context) -> Result {
151        let mut remaining_tries = 50;
152
153        loop {
154            let line = self.inner.next_line(context).await?;
155
156            let env = self.env.borrow();
157
158            if !line.is_empty()
159                || env.options.get(Interactive) == Off
160                || remaining_tries == 0
161                || !env.system.isatty(self.fd)
162            {
163                return Ok(line);
164            }
165
166            if env.options.get(PosixlyCorrect) == Off
167                && let Some(config) = env.any.get::<SuspendedJobsGuardConfig>()
168                && env.jobs.iter().any(|(_, job)| job.state.is_stopped())
169            {
170                env.system.print_error(&config.message).await;
171            } else if env.options.get(IgnoreEofOption) == On
172                && let Some(config) = env.any.get::<IgnoreEofConfig>()
173            {
174                env.system.print_error(&config.message).await;
175            } else {
176                return Ok(line);
177            }
178
179            remaining_tries -= 1;
180        }
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::super::Memory;
187    use super::*;
188    use crate::job::{Job, Pid, ProcessState};
189    use crate::option::On;
190    use crate::system::r#virtual::{FdBody, FileBody, Inode, OpenFileDescription, VirtualSystem};
191    use crate::system::{Concurrent, Mode};
192    use crate::test_helper::assert_stderr;
193    use enumset::EnumSet;
194    use futures_util::FutureExt as _;
195    use std::rc::Rc;
196
197    fn set_stdin_to_tty(system: &mut VirtualSystem) {
198        system
199            .current_process_mut()
200            .set_fd(
201                Fd::STDIN,
202                FdBody {
203                    open_file_description: Rc::new(RefCell::new(OpenFileDescription::new(
204                        Rc::new(RefCell::new(Inode {
205                            body: FileBody::Terminal { content: vec![] },
206                            permissions: Mode::empty(),
207                        })),
208                        /* offset = */ 0,
209                        /* is_readable = */ true,
210                        /* is_writable = */ true,
211                        /* is_appending = */ false,
212                        /* is_nonblocking = */ false,
213                    ))),
214                    flags: EnumSet::empty(),
215                },
216            )
217            .unwrap();
218    }
219
220    fn set_stdin_to_regular_file(system: &mut VirtualSystem) {
221        system
222            .current_process_mut()
223            .set_fd(
224                Fd::STDIN,
225                FdBody {
226                    open_file_description: Rc::new(RefCell::new(OpenFileDescription::new(
227                        Rc::new(RefCell::new(Inode {
228                            body: FileBody::Regular {
229                                content: vec![],
230                                is_native_executable: false,
231                            },
232                            permissions: Mode::empty(),
233                        })),
234                        /* offset = */ 0,
235                        /* is_readable = */ true,
236                        /* is_writable = */ true,
237                        /* is_appending = */ false,
238                        /* is_nonblocking = */ false,
239                    ))),
240                    flags: EnumSet::empty(),
241                },
242            )
243            .unwrap();
244    }
245
246    /// `Input` decorator that returns EOF for the first `count` calls
247    /// and then reads from the inner input.
248    struct EofStub<T> {
249        inner: T,
250        count: usize,
251    }
252
253    impl<T: Input> Input for EofStub<T> {
254        async fn next_line(&mut self, context: &Context) -> Result {
255            if let Some(remaining) = self.count.checked_sub(1) {
256                self.count = remaining;
257                Ok("".to_string())
258            } else {
259                self.inner.next_line(context).await
260            }
261        }
262    }
263
264    #[test]
265    fn decorator_reads_from_inner_input() {
266        let mut system = VirtualSystem::new();
267        set_stdin_to_tty(&mut system);
268        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
269        env.options.set(Interactive, On);
270        env.options.set(IgnoreEofOption, On);
271        env.any.insert(Box::new(IgnoreEofConfig::default()));
272        env.any
273            .insert(Box::new(SuspendedJobsGuardConfig::default()));
274        let ref_env = RefCell::new(&mut env);
275        let mut decorator = EofGuard::new(Memory::new("echo foo\n"), Fd::STDIN, &ref_env);
276
277        let result = decorator
278            .next_line(&Context::default())
279            .now_or_never()
280            .unwrap();
281        assert_eq!(result.unwrap(), "echo foo\n");
282    }
283
284    #[test]
285    fn decorator_reads_input_again_on_eof_with_ignore_eof_option() {
286        let mut system = VirtualSystem::new();
287        set_stdin_to_tty(&mut system);
288        let state = system.state.clone();
289        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
290        env.options.set(Interactive, On);
291        env.options.set(IgnoreEofOption, On);
292        env.any.insert(Box::new(IgnoreEofConfig {
293            message: "EOF ignored\n".to_string(),
294        }));
295        let ref_env = RefCell::new(&mut env);
296        let mut decorator = EofGuard::new(
297            EofStub {
298                inner: Memory::new("echo foo\n"),
299                count: 1,
300            },
301            Fd::STDIN,
302            &ref_env,
303        );
304
305        let result = decorator
306            .next_line(&Context::default())
307            .now_or_never()
308            .unwrap();
309        assert_eq!(result.unwrap(), "echo foo\n");
310        assert_stderr(&state, |stderr| assert_eq!(stderr, "EOF ignored\n"));
311    }
312
313    #[test]
314    fn decorator_reads_input_up_to_50_times() {
315        let mut system = VirtualSystem::new();
316        set_stdin_to_tty(&mut system);
317        let state = system.state.clone();
318        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
319        env.options.set(Interactive, On);
320        env.options.set(IgnoreEofOption, On);
321        env.any.insert(Box::new(IgnoreEofConfig {
322            message: "EOF ignored\n".to_string(),
323        }));
324        let ref_env = RefCell::new(&mut env);
325        let mut decorator = EofGuard::new(
326            EofStub {
327                inner: Memory::new("echo foo\n"),
328                count: 50,
329            },
330            Fd::STDIN,
331            &ref_env,
332        );
333
334        let result = decorator
335            .next_line(&Context::default())
336            .now_or_never()
337            .unwrap();
338        assert_eq!(result.unwrap(), "echo foo\n");
339        assert_stderr(&state, |stderr| {
340            assert_eq!(stderr, "EOF ignored\n".repeat(50))
341        });
342    }
343
344    #[test]
345    fn decorator_returns_empty_line_after_reading_51_times() {
346        let mut system = VirtualSystem::new();
347        set_stdin_to_tty(&mut system);
348        let state = system.state.clone();
349        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
350        env.options.set(Interactive, On);
351        env.options.set(IgnoreEofOption, On);
352        env.any.insert(Box::new(IgnoreEofConfig {
353            message: "EOF ignored\n".to_string(),
354        }));
355        let ref_env = RefCell::new(&mut env);
356        let mut decorator = EofGuard::new(
357            EofStub {
358                inner: Memory::new("echo foo\n"),
359                count: 51,
360            },
361            Fd::STDIN,
362            &ref_env,
363        );
364
365        let result = decorator
366            .next_line(&Context::default())
367            .now_or_never()
368            .unwrap();
369        assert_eq!(result.unwrap(), "");
370        assert_stderr(&state, |stderr| {
371            assert_eq!(stderr, "EOF ignored\n".repeat(50))
372        });
373    }
374
375    #[test]
376    fn decorator_returns_immediately_if_not_interactive() {
377        let mut system = VirtualSystem::new();
378        set_stdin_to_tty(&mut system);
379        let state = system.state.clone();
380        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
381        // Interactive is Off (default)
382        env.options.set(IgnoreEofOption, On);
383        env.any.insert(Box::new(IgnoreEofConfig::default()));
384        env.any
385            .insert(Box::new(SuspendedJobsGuardConfig::default()));
386        let ref_env = RefCell::new(&mut env);
387        let mut decorator = EofGuard::new(
388            EofStub {
389                inner: Memory::new("echo foo\n"),
390                count: 1,
391            },
392            Fd::STDIN,
393            &ref_env,
394        );
395
396        let result = decorator
397            .next_line(&Context::default())
398            .now_or_never()
399            .unwrap();
400        assert_eq!(result.unwrap(), "");
401        assert_stderr(&state, |stderr| assert_eq!(stderr, ""));
402    }
403
404    #[test]
405    fn decorator_returns_immediately_if_not_ignore_eof_and_no_suspended_jobs() {
406        let mut system = VirtualSystem::new();
407        set_stdin_to_tty(&mut system);
408        let state = system.state.clone();
409        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
410        env.options.set(Interactive, On);
411        // IgnoreEof is Off (default), no jobs
412        env.any.insert(Box::new(IgnoreEofConfig::default()));
413        env.any
414            .insert(Box::new(SuspendedJobsGuardConfig::default()));
415        let ref_env = RefCell::new(&mut env);
416        let mut decorator = EofGuard::new(
417            EofStub {
418                inner: Memory::new("echo foo\n"),
419                count: 1,
420            },
421            Fd::STDIN,
422            &ref_env,
423        );
424
425        let result = decorator
426            .next_line(&Context::default())
427            .now_or_never()
428            .unwrap();
429        assert_eq!(result.unwrap(), "");
430        assert_stderr(&state, |stderr| assert_eq!(stderr, ""));
431    }
432
433    #[test]
434    fn decorator_returns_immediately_if_not_terminal() {
435        let mut system = VirtualSystem::new();
436        set_stdin_to_regular_file(&mut system);
437        let state = system.state.clone();
438        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
439        env.options.set(Interactive, On);
440        env.options.set(IgnoreEofOption, On);
441        env.any.insert(Box::new(IgnoreEofConfig::default()));
442        env.any
443            .insert(Box::new(SuspendedJobsGuardConfig::default()));
444        let ref_env = RefCell::new(&mut env);
445        let mut decorator = EofGuard::new(
446            EofStub {
447                inner: Memory::new("echo foo\n"),
448                count: 1,
449            },
450            Fd::STDIN,
451            &ref_env,
452        );
453
454        let result = decorator
455            .next_line(&Context::default())
456            .now_or_never()
457            .unwrap();
458        assert_eq!(result.unwrap(), "");
459        assert_stderr(&state, |stderr| assert_eq!(stderr, ""));
460    }
461
462    // Counterpart to `decorator_reads_input_again_on_eof_with_ignore_eof_option`:
463    // without `IgnoreEofConfig` in `env.any`, the ignore-eof retry is disabled,
464    // so the decorator returns the EOF immediately.
465    #[test]
466    fn decorator_returns_immediately_if_no_ignore_eof_config() {
467        let mut system = VirtualSystem::new();
468        set_stdin_to_tty(&mut system);
469        let state = system.state.clone();
470        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
471        env.options.set(Interactive, On);
472        env.options.set(IgnoreEofOption, On);
473        // No IgnoreEofConfig in env.any
474        let ref_env = RefCell::new(&mut env);
475        let mut decorator = EofGuard::new(
476            EofStub {
477                inner: Memory::new("echo foo\n"),
478                count: 1,
479            },
480            Fd::STDIN,
481            &ref_env,
482        );
483
484        let result = decorator
485            .next_line(&Context::default())
486            .now_or_never()
487            .unwrap();
488        assert_eq!(result.unwrap(), "");
489        assert_stderr(&state, |stderr| assert_eq!(stderr, ""));
490    }
491
492    #[test]
493    fn decorator_ignores_eof_when_there_are_suspended_jobs() {
494        let mut system = VirtualSystem::new();
495        set_stdin_to_tty(&mut system);
496        let state = system.state.clone();
497        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
498        env.options.set(Interactive, On);
499        // IgnoreEof is Off, but there is a suspended job
500        let mut job = Job::new(Pid(42));
501        job.state = ProcessState::stopped(crate::system::r#virtual::SIGTSTP);
502        env.jobs.insert(job);
503        env.any.insert(Box::new(SuspendedJobsGuardConfig {
504            message: "There are stopped jobs.\n".to_string(),
505        }));
506        let ref_env = RefCell::new(&mut env);
507        let mut decorator = EofGuard::new(
508            EofStub {
509                inner: Memory::new("echo foo\n"),
510                count: 1,
511            },
512            Fd::STDIN,
513            &ref_env,
514        );
515
516        let result = decorator
517            .next_line(&Context::default())
518            .now_or_never()
519            .unwrap();
520        assert_eq!(result.unwrap(), "echo foo\n");
521        assert_stderr(&state, |stderr| {
522            assert_eq!(stderr, "There are stopped jobs.\n")
523        });
524    }
525
526    #[test]
527    fn decorator_returns_immediately_if_posixly_correct_with_suspended_jobs() {
528        let mut system = VirtualSystem::new();
529        set_stdin_to_tty(&mut system);
530        let state = system.state.clone();
531        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
532        env.options.set(Interactive, On);
533        env.options.set(PosixlyCorrect, On);
534        let mut job = Job::new(Pid(42));
535        job.state = ProcessState::stopped(crate::system::r#virtual::SIGTSTP);
536        env.jobs.insert(job);
537        env.any.insert(Box::new(SuspendedJobsGuardConfig {
538            message: "There are stopped jobs.\n".to_string(),
539        }));
540        let ref_env = RefCell::new(&mut env);
541        let mut decorator = EofGuard::new(
542            EofStub {
543                inner: Memory::new("echo foo\n"),
544                count: 1,
545            },
546            Fd::STDIN,
547            &ref_env,
548        );
549
550        let result = decorator
551            .next_line(&Context::default())
552            .now_or_never()
553            .unwrap();
554        assert_eq!(result.unwrap(), "");
555        assert_stderr(&state, |stderr| assert_eq!(stderr, ""));
556    }
557
558    // Counterpart to `decorator_ignores_eof_when_there_are_suspended_jobs`:
559    // without `SuspendedJobsGuardConfig` in `env.any`, the suspended-jobs guard
560    // is disabled, so the decorator returns the EOF immediately.
561    #[test]
562    fn decorator_returns_immediately_if_no_suspended_jobs_config() {
563        let mut system = VirtualSystem::new();
564        set_stdin_to_tty(&mut system);
565        let state = system.state.clone();
566        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
567        env.options.set(Interactive, On);
568        // IgnoreEof is Off, but there is a suspended job
569        let mut job = Job::new(Pid(42));
570        job.state = ProcessState::stopped(crate::system::r#virtual::SIGTSTP);
571        env.jobs.insert(job);
572        // No SuspendedJobsGuardConfig in env.any
573        let ref_env = RefCell::new(&mut env);
574        let mut decorator = EofGuard::new(
575            EofStub {
576                inner: Memory::new("echo foo\n"),
577                count: 1,
578            },
579            Fd::STDIN,
580            &ref_env,
581        );
582
583        let result = decorator
584            .next_line(&Context::default())
585            .now_or_never()
586            .unwrap();
587        assert_eq!(result.unwrap(), "");
588        assert_stderr(&state, |stderr| assert_eq!(stderr, ""));
589    }
590
591    // The two guards are independent: even when a suspended job exists, if the
592    // suspended-jobs guard is not configured, the ignore-eof guard still applies
593    // when the `ignore-eof` option is on and `IgnoreEofConfig` is present.
594    #[test]
595    fn decorator_falls_back_to_ignore_eof_when_no_suspended_jobs_config() {
596        let mut system = VirtualSystem::new();
597        set_stdin_to_tty(&mut system);
598        let state = system.state.clone();
599        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
600        env.options.set(Interactive, On);
601        env.options.set(IgnoreEofOption, On);
602        let mut job = Job::new(Pid(42));
603        job.state = ProcessState::stopped(crate::system::r#virtual::SIGTSTP);
604        env.jobs.insert(job);
605        // Only IgnoreEofConfig is present; SuspendedJobsGuardConfig is absent
606        env.any.insert(Box::new(IgnoreEofConfig {
607            message: "EOF ignored\n".to_string(),
608        }));
609        let ref_env = RefCell::new(&mut env);
610        let mut decorator = EofGuard::new(
611            EofStub {
612                inner: Memory::new("echo foo\n"),
613                count: 1,
614            },
615            Fd::STDIN,
616            &ref_env,
617        );
618
619        let result = decorator
620            .next_line(&Context::default())
621            .now_or_never()
622            .unwrap();
623        assert_eq!(result.unwrap(), "echo foo\n");
624        assert_stderr(&state, |stderr| assert_eq!(stderr, "EOF ignored\n"));
625    }
626
627    #[test]
628    fn suspended_jobs_message_takes_priority_over_ignore_eof_message() {
629        // The ignore-eof message typically tells the user to type `exit` to
630        // leave the shell, but a plain `exit` does not work when there are
631        // suspended jobs. The suspended-jobs message must take priority so the
632        // user is correctly directed to use `exit -f` instead.
633        let mut system = VirtualSystem::new();
634        set_stdin_to_tty(&mut system);
635        let state = system.state.clone();
636        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
637        env.options.set(Interactive, On);
638        env.options.set(IgnoreEofOption, On);
639        let mut job = Job::new(Pid(42));
640        job.state = ProcessState::stopped(crate::system::r#virtual::SIGTSTP);
641        env.jobs.insert(job);
642        env.any.insert(Box::new(IgnoreEofConfig {
643            message: "EOF ignored\n".to_string(),
644        }));
645        env.any.insert(Box::new(SuspendedJobsGuardConfig {
646            message: "There are stopped jobs.\n".to_string(),
647        }));
648        let ref_env = RefCell::new(&mut env);
649        let mut decorator = EofGuard::new(
650            EofStub {
651                inner: Memory::new("echo foo\n"),
652                count: 1,
653            },
654            Fd::STDIN,
655            &ref_env,
656        );
657
658        let result = decorator
659            .next_line(&Context::default())
660            .now_or_never()
661            .unwrap();
662        assert_eq!(result.unwrap(), "echo foo\n");
663        assert_stderr(&state, |stderr| {
664            assert_eq!(stderr, "There are stopped jobs.\n")
665        });
666    }
667}