1mod error;
66pub mod keep_alive;
67pub mod mach_services;
68pub mod process_type;
69pub mod resource_limits;
70pub mod sockets;
71
72pub use self::error::Error;
73pub use self::keep_alive::{KeepAliveOptions, KeepAliveType};
74pub use self::mach_services::{MachServiceEntry, MachServiceOptions};
75pub use self::process_type::ProcessType;
76pub use self::resource_limits::ResourceLimits;
77pub use self::sockets::{BonjourType, Socket, SocketOptions, Sockets};
78
79#[cfg(feature = "cron")]
80use cron::{Schedule, TimeUnitSpec};
81#[cfg(feature = "plist")]
82use plist::Value;
83#[cfg(feature = "io")]
84use plist::{from_bytes, from_file, from_reader, from_reader_xml};
85#[cfg(feature = "io")]
86use plist::{to_file_binary, to_file_xml, to_writer_binary, to_writer_xml};
87#[cfg(feature = "serde")]
88use serde::{Deserialize, Serialize};
89use std::collections::HashMap;
90#[cfg(feature = "cron")]
91use std::convert::TryInto;
92#[cfg(feature = "io")]
93use std::io::{Read, Seek, Write};
94use std::path::Path;
95
96#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
124#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
125#[cfg_attr(feature = "io", serde(rename_all = "PascalCase"))]
126#[derive(Debug, Default, PartialEq)]
127pub struct Launchd {
128 label: String,
129 disabled: Option<bool>,
130 user_name: Option<String>,
131 group_name: Option<String>,
132 #[cfg_attr(feature = "serde", serde(rename = "inetdCompatibility"))]
133 inetd_compatibility: Option<HashMap<InetdCompatibility, bool>>,
134 limit_load_to_hosts: Option<Vec<String>>,
135 limit_load_from_hosts: Option<Vec<String>>,
136 limit_load_to_session_type: Option<LoadSessionType>,
137 limit_load_to_hardware: Option<HashMap<String, Vec<String>>>,
138 limit_load_from_hardware: Option<HashMap<String, Vec<String>>>,
139 program: Option<String>, bundle_program: Option<String>,
141 program_arguments: Option<Vec<String>>,
142 enable_globbing: Option<bool>,
143 enable_transactions: Option<bool>,
144 enable_pressured_exit: Option<bool>,
145 on_demand: Option<bool>, #[cfg_attr(feature = "serde", serde(rename = "ServiceIPC"))]
147 service_ipc: Option<bool>, keep_alive: Option<KeepAliveType>,
149 run_at_load: Option<bool>,
150 root_directory: Option<String>,
151 working_directory: Option<String>,
152 environment_variables: Option<HashMap<String, String>>,
153 umask: Option<u16>, time_out: Option<u32>,
155 exit_time_out: Option<u32>,
156 throttle_interval: Option<u32>,
157 init_groups: Option<bool>,
158 watch_paths: Option<Vec<String>>,
159 queue_directories: Option<Vec<String>>,
160 start_on_mount: Option<bool>,
161 start_interval: Option<u32>,
162 start_calendar_intervals: Option<Vec<CalendarInterval>>,
163 standard_in_path: Option<String>,
164 standard_out_path: Option<String>,
165 standard_error_path: Option<String>,
166 debug: Option<bool>,
167 wait_for_debugger: Option<bool>,
168 soft_resource_limits: Option<ResourceLimits>,
169 hard_resource_limits: Option<ResourceLimits>,
170 nice: Option<i32>,
171 process_type: Option<ProcessType>,
172 abandon_process_group: Option<bool>,
173 #[cfg_attr(feature = "serde", serde(rename = "LowPriorityIO"))]
174 low_priority_io: Option<bool>,
175 #[cfg_attr(feature = "serde", serde(rename = "LowPriorityBackgroundIO"))]
176 low_priority_background_io: Option<bool>,
177 materialize_dataless_files: Option<bool>,
178 launch_only_once: Option<bool>,
179 mach_services: Option<HashMap<String, MachServiceEntry>>,
180 sockets: Option<Sockets>,
181 launch_events: Option<LaunchEvents>,
182 hopefully_exits_last: Option<bool>, hopefully_exits_first: Option<bool>, session_create: Option<bool>,
185 legacy_timers: Option<bool>, }
188
189#[cfg(feature = "plist")]
193type LaunchEvents = HashMap<String, HashMap<String, HashMap<String, Value>>>;
194
195#[cfg(not(feature = "plist"))]
197type LaunchEvents = ();
198
199#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
213#[cfg_attr(feature = "io", serde(rename_all = "PascalCase"))]
214#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
215pub struct CalendarInterval {
216 minute: Option<u8>,
217 hour: Option<u8>,
218 day: Option<u8>,
219 weekday: Option<u8>,
220 month: Option<u8>,
221}
222
223#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
224#[derive(Debug, Clone, PartialEq, Eq, Hash)]
225pub enum InetdCompatibility {
226 Wait, }
228
229#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
231#[cfg_attr(feature = "serde", serde(untagged))]
232#[derive(Debug, Clone, PartialEq, Eq)]
233pub enum LoadSessionType {
234 BareString(String),
235 Array(Vec<String>),
236}
237
238impl From<String> for LoadSessionType {
239 fn from(value: String) -> Self {
240 LoadSessionType::BareString(value)
241 }
242}
243
244impl From<&str> for LoadSessionType {
245 fn from(value: &str) -> Self {
246 LoadSessionType::BareString(value.to_owned())
247 }
248}
249
250impl From<Vec<String>> for LoadSessionType {
251 fn from(value: Vec<String>) -> Self {
252 LoadSessionType::Array(value)
253 }
254}
255
256impl From<Vec<&str>> for LoadSessionType {
257 fn from(value: Vec<&str>) -> Self {
258 LoadSessionType::Array(value.into_iter().map(|s| s.to_owned()).collect())
259 }
260}
261
262impl Launchd {
264 pub fn new<S: AsRef<str>, P: AsRef<Path>>(label: S, program: P) -> Result<Self, Error> {
265 let pathstr = program
266 .as_ref()
267 .to_str()
268 .ok_or(Error::PathConversion)?
269 .to_owned();
270 Ok(Launchd {
271 label: String::from(label.as_ref()),
272 program: Some(pathstr),
273 ..Default::default()
274 })
275 }
276
277 pub fn with_label<S: AsRef<str>>(mut self, label: S) -> Self {
278 self.label = String::from(label.as_ref());
279 self
280 }
281
282 pub fn with_disabled(mut self, disabled: bool) -> Self {
283 self.disabled = Some(disabled);
284 self
285 }
286
287 pub fn disabled(self) -> Self {
288 self.with_disabled(true)
289 }
290
291 pub fn with_user_name<S: AsRef<str>>(mut self, user_name: S) -> Self {
292 self.user_name = Some(String::from(user_name.as_ref()));
293 self
294 }
295
296 pub fn with_group_name<S: AsRef<str>>(mut self, group_name: S) -> Self {
297 self.group_name = Some(String::from(group_name.as_ref()));
298 self
299 }
300
301 pub fn with_program<P: AsRef<Path>>(mut self, program: P) -> Result<Self, Error> {
302 let pathstr = program
303 .as_ref()
304 .to_str()
305 .ok_or(Error::PathConversion)?
306 .to_owned();
307 self.program = Some(pathstr);
308 Ok(self)
309 }
310
311 pub fn with_bundle_program<S: AsRef<str>>(mut self, bundle: S) -> Self {
312 self.bundle_program = Some(String::from(bundle.as_ref()));
313 self
314 }
315
316 pub fn with_program_arguments(mut self, program_arguments: Vec<String>) -> Self {
317 self.program_arguments = Some(program_arguments);
318 self
319 }
320
321 pub fn with_run_at_load(mut self, run_at_load: bool) -> Self {
322 self.run_at_load = Some(run_at_load);
323 self
324 }
325
326 pub fn run_at_load(self) -> Self {
327 self.with_run_at_load(true)
328 }
329
330 pub fn with_queue_directories(mut self, queue_directories: Vec<String>) -> Self {
331 self.queue_directories = Some(queue_directories);
332 self
333 }
334
335 pub fn with_watch_paths(mut self, watch_paths: Vec<String>) -> Self {
336 self.watch_paths = Some(watch_paths);
337 self
338 }
339
340 pub fn with_start_on_mount(mut self, start_on_mount: bool) -> Self {
341 self.start_on_mount = Some(start_on_mount);
342 self
343 }
344
345 pub fn start_on_mount(self) -> Self {
346 self.with_start_on_mount(true)
347 }
348
349 pub fn with_start_interval(mut self, start_interval: u32) -> Self {
350 self.start_interval = Some(start_interval);
351 self
352 }
353
354 pub fn with_start_calendar_intervals(
355 mut self,
356 start_calendar_intervals: Vec<CalendarInterval>,
357 ) -> Self {
358 self.start_calendar_intervals = Some(start_calendar_intervals);
359 self
360 }
361
362 pub fn with_abandon_process_group(mut self, value: bool) -> Self {
363 self.abandon_process_group = Some(value);
364 self
365 }
366
367 pub fn abandon_process_group(self) -> Self {
368 self.with_abandon_process_group(true)
369 }
370
371 pub fn with_debug(mut self, value: bool) -> Self {
372 self.debug = Some(value);
373 self
374 }
375
376 pub fn debug(self) -> Self {
377 self.with_debug(true)
378 }
379
380 pub fn with_enable_globbing(mut self, value: bool) -> Self {
381 self.enable_globbing = Some(value);
382 self
383 }
384
385 pub fn enable_globbing(self) -> Self {
386 self.with_enable_globbing(true)
387 }
388
389 pub fn with_enable_transactions(mut self, value: bool) -> Self {
390 self.enable_transactions = Some(value);
391 self
392 }
393
394 pub fn enable_transactions(self) -> Self {
395 self.with_enable_transactions(true)
396 }
397
398 pub fn with_enable_pressured_exit(mut self, value: bool) -> Self {
399 self.enable_pressured_exit = Some(value);
400 self
401 }
402
403 pub fn enable_pressured_exit(self) -> Self {
404 self.with_enable_pressured_exit(true)
405 }
406
407 pub fn with_environment_variables(mut self, env: HashMap<String, String>) -> Self {
408 self.environment_variables = Some(env);
409 self
410 }
411
412 pub fn with_exit_timeout(mut self, timeout: u32) -> Self {
413 self.exit_time_out = Some(timeout);
414 self
415 }
416
417 pub fn with_init_groups(mut self, value: bool) -> Self {
418 self.init_groups = Some(value);
419 self
420 }
421
422 pub fn init_groups(self) -> Self {
423 self.with_init_groups(true)
424 }
425
426 pub fn with_launch_only_once(mut self, value: bool) -> Self {
427 self.launch_only_once = Some(value);
428 self
429 }
430
431 pub fn launch_only_once(self) -> Self {
432 self.with_launch_only_once(true)
433 }
434
435 pub fn with_limit_load_from_hosts(mut self, value: Vec<String>) -> Self {
436 self.limit_load_from_hosts = Some(value);
437 self
438 }
439
440 pub fn with_limit_to_from_hosts(mut self, value: Vec<String>) -> Self {
441 self.limit_load_to_hosts = Some(value);
442 self
443 }
444
445 pub fn with_limit_load_to_session_type(mut self, value: LoadSessionType) -> Self {
446 self.limit_load_to_session_type = Some(value);
447 self
448 }
449
450 pub fn with_limit_load_to_hardware(mut self, value: HashMap<String, Vec<String>>) -> Self {
451 self.limit_load_to_hardware = Some(value);
452 self
453 }
454
455 pub fn with_limit_load_from_hardware(mut self, value: HashMap<String, Vec<String>>) -> Self {
456 self.limit_load_from_hardware = Some(value);
457 self
458 }
459
460 pub fn with_low_priority_io(mut self, value: bool) -> Self {
461 self.low_priority_io = Some(value);
462 self
463 }
464
465 pub fn low_priority_io(self) -> Self {
466 self.with_low_priority_io(true)
467 }
468
469 pub fn with_low_priority_background_io(mut self, value: bool) -> Self {
470 self.low_priority_background_io = Some(value);
471 self
472 }
473
474 pub fn low_priority_background_io(self) -> Self {
475 self.with_low_priority_background_io(true)
476 }
477
478 pub fn with_mach_services(mut self, services: HashMap<String, MachServiceEntry>) -> Self {
479 self.mach_services = Some(services);
480 self
481 }
482
483 pub fn with_nice(mut self, nice: i32) -> Self {
484 self.nice = Some(nice);
485 self
486 }
487
488 pub fn with_root_directory<P: AsRef<Path>>(mut self, path: P) -> Result<Self, Error> {
489 let pathstr = path
490 .as_ref()
491 .to_str()
492 .ok_or(Error::PathConversion)?
493 .to_owned();
494 self.root_directory = Some(pathstr);
495 Ok(self)
496 }
497
498 pub fn with_standard_error_path<P: AsRef<Path>>(mut self, path: P) -> Result<Self, Error> {
499 let pathstr = path
500 .as_ref()
501 .to_str()
502 .ok_or(Error::PathConversion)?
503 .to_owned();
504 self.standard_error_path = Some(pathstr);
505 Ok(self)
506 }
507
508 pub fn with_standard_in_path<P: AsRef<Path>>(mut self, path: P) -> Result<Self, Error> {
509 let pathstr = path
510 .as_ref()
511 .to_str()
512 .ok_or(Error::PathConversion)?
513 .to_owned();
514 self.standard_in_path = Some(pathstr);
515 Ok(self)
516 }
517
518 pub fn with_standard_out_path<P: AsRef<Path>>(mut self, path: P) -> Result<Self, Error> {
519 let pathstr = path
520 .as_ref()
521 .to_str()
522 .ok_or(Error::PathConversion)?
523 .to_owned();
524 self.standard_out_path = Some(pathstr);
525 Ok(self)
526 }
527
528 pub fn with_throttle_interval(mut self, value: u32) -> Self {
529 self.throttle_interval = Some(value);
530 self
531 }
532
533 pub fn with_timeout(mut self, timeout: u32) -> Self {
534 self.time_out = Some(timeout);
535 self
536 }
537
538 pub fn with_umask(mut self, umask: u16) -> Self {
539 self.umask = Some(umask);
540 self
541 }
542
543 pub fn with_wait_for_debugger(mut self, value: bool) -> Self {
544 self.wait_for_debugger = Some(value);
545 self
546 }
547
548 pub fn wait_for_debugger(self) -> Self {
549 self.with_wait_for_debugger(true)
550 }
551
552 pub fn with_materialize_dataless_files(mut self, value: bool) -> Self {
553 self.materialize_dataless_files = Some(value);
554 self
555 }
556
557 pub fn materialize_dataless_files(self) -> Self {
558 self.with_materialize_dataless_files(true)
559 }
560
561 pub fn with_working_directory<P: AsRef<Path>>(mut self, path: P) -> Result<Self, Error> {
562 let pathstr = path
563 .as_ref()
564 .to_str()
565 .ok_or(Error::PathConversion)?
566 .to_owned();
567 self.working_directory = Some(pathstr);
568 Ok(self)
569 }
570
571 pub fn with_inetd_compatibility(mut self, wait: bool) -> Self {
572 self.inetd_compatibility = Some(HashMap::from([(InetdCompatibility::Wait, wait)]));
573 self
574 }
575
576 pub fn with_keep_alive(mut self, keep_alive: KeepAliveType) -> Self {
577 self.keep_alive = Some(keep_alive);
578 self
579 }
580
581 pub fn with_process_type(mut self, process_type: ProcessType) -> Self {
582 self.process_type = Some(process_type);
583 self
584 }
585
586 pub fn with_hard_resource_limits(mut self, limits: ResourceLimits) -> Self {
587 self.hard_resource_limits = Some(limits);
588 self
589 }
590
591 pub fn with_soft_resource_limits(mut self, limits: ResourceLimits) -> Self {
592 self.soft_resource_limits = Some(limits);
593 self
594 }
595
596 pub fn with_socket(mut self, socket: Sockets) -> Self {
597 if let Some(sockets) = self.sockets.take() {
598 match (sockets, socket) {
599 (Sockets::Array(mut arr), Sockets::Array(mut new_arr)) => {
600 arr.append(&mut new_arr);
601 self.sockets = Some(Sockets::Array(arr));
602 }
603 (Sockets::Array(mut arr), Sockets::Dictionary(new_dict)) => {
604 arr.push(new_dict);
605 self.sockets = Some(Sockets::Array(arr));
606 }
607 (Sockets::Dictionary(dict), Sockets::Dictionary(new_dict)) => {
608 self.sockets = Some(Sockets::Array(vec![dict, new_dict]))
609 }
610 (Sockets::Dictionary(dict), Sockets::Array(mut new_arr)) => {
611 new_arr.insert(0, dict);
612 self.sockets = Some(Sockets::Array(new_arr));
613 }
614 }
615 } else {
616 self.sockets = Some(socket);
617 }
618 self
619 }
620
621 pub fn with_launch_events(mut self, value: LaunchEvents) -> Self {
622 self.launch_events = Some(value);
623 self
624 }
625
626 pub fn with_session_create(mut self, value: bool) -> Self {
627 self.session_create = Some(value);
628 self
629 }
630
631 pub fn session_create(self) -> Self {
632 self.with_session_create(true)
633 }
634}
635
636#[cfg(feature = "io")]
637impl Launchd {
638 pub fn to_writer_xml<W: Write>(&self, writer: W) -> Result<(), Error> {
640 to_writer_xml(writer, self).map_err(Error::Write)
641 }
642
643 pub fn to_file_xml<P: AsRef<Path>>(&self, file: P) -> Result<(), Error> {
644 to_file_xml(file, self).map_err(Error::Write)
645 }
646
647 pub fn to_writer_binary<W: Write>(&self, writer: W) -> Result<(), Error> {
648 to_writer_binary(writer, self).map_err(Error::Write)
649 }
650
651 pub fn to_file_binary<P: AsRef<Path>>(&self, file: P) -> Result<(), Error> {
652 to_file_binary(file, self).map_err(Error::Write)
653 }
654
655 pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
657 from_bytes(bytes).map_err(Error::Read)
658 }
659
660 pub fn from_file<P: AsRef<Path>>(file: P) -> Result<Self, Error> {
661 from_file(file).map_err(Error::Read)
662 }
663
664 pub fn from_reader<R: Read + Seek>(reader: R) -> Result<Self, Error> {
665 from_reader(reader).map_err(Error::Read)
666 }
667
668 pub fn from_reader_xml<R: Read + Seek>(reader: R) -> Result<Self, Error> {
669 from_reader_xml(reader).map_err(Error::Read)
670 }
671}
672
673impl CalendarInterval {
674 #[cfg(feature = "cron")] fn is_initialized(&self) -> bool {
676 self.minute.is_some()
677 || self.hour.is_some()
678 || self.day.is_some()
679 || self.weekday.is_some()
680 || self.month.is_some()
681 }
682
683 pub fn with_minute(self, minute: u8) -> Result<Self, Error> {
684 if minute > 59 {
685 Err(Error::CalendarFieldOutOfBounds(0..=59, minute))
686 } else {
687 let mut result = self;
688 result.minute = Some(minute);
689 Ok(result)
690 }
691 }
692
693 pub fn with_hour(self, hour: u8) -> Result<Self, Error> {
694 if hour > 23 {
695 Err(Error::CalendarFieldOutOfBounds(0..=23, hour))
696 } else {
697 let mut result = self;
698 result.hour = Some(hour);
699 Ok(result)
700 }
701 }
702
703 pub fn with_day(self, day: u8) -> Result<Self, Error> {
704 if day == 0 || day > 31 {
705 Err(Error::CalendarFieldOutOfBounds(1..=31, day))
706 } else {
707 let mut result = self;
708 result.day = Some(day);
709 Ok(result)
710 }
711 }
712
713 pub fn with_weekday(self, weekday: u8) -> Result<Self, Error> {
714 if weekday > 7 {
715 Err(Error::CalendarFieldOutOfBounds(0..=7, weekday))
716 } else {
717 let mut result = self;
718 result.weekday = Some(weekday);
719 Ok(result)
720 }
721 }
722
723 pub fn with_month(self, month: u8) -> Result<Self, Error> {
724 if month == 0 || month > 12 {
725 Err(Error::CalendarFieldOutOfBounds(1..=12, month))
726 } else {
727 let mut result = self;
728 result.month = Some(month);
729 Ok(result)
730 }
731 }
732
733 #[cfg(feature = "cron")]
734 pub fn from_cron_schedule(schedule: Schedule) -> Result<Vec<Self>, Error> {
735 let mut result_vec = Vec::new();
736 for month in schedule.months().iter() {
737 for weekday in schedule.days_of_week().iter() {
738 for day in schedule.days_of_month().iter() {
739 for hour in schedule.hours().iter() {
740 for minute in schedule.minutes().iter() {
741 let result = Self::default();
742
743 if !schedule.months().is_all() {
745 result.with_month(
746 month
747 .try_into()
748 .map_err(|_| Error::InvalidCronField(month))?,
749 )?;
750 }
751 if !schedule.days_of_week().is_all() {
752 result.with_weekday(
753 weekday
754 .try_into()
755 .map_err(|_| Error::InvalidCronField(weekday))?,
756 )?;
757 }
758 if !schedule.days_of_month().is_all() {
759 result.with_day(
760 day.try_into().map_err(|_| Error::InvalidCronField(day))?,
761 )?;
762 }
763 if !schedule.hours().is_all() {
764 result.with_hour(
765 hour.try_into().map_err(|_| Error::InvalidCronField(hour))?,
766 )?;
767 }
768 if !schedule.minutes().is_all() {
769 result.with_minute(
770 minute
771 .try_into()
772 .map_err(|_| Error::InvalidCronField(minute))?,
773 )?;
774 }
775
776 if result.is_initialized() {
777 result_vec.push(result);
778 }
779
780 if schedule.minutes().is_all() {
781 break;
782 }
783 }
784 if schedule.hours().is_all() {
785 break;
786 }
787 }
788 if schedule.days_of_month().is_all() {
789 break;
790 }
791 }
792 if schedule.days_of_week().is_all() {
793 break;
794 }
795 }
796 if schedule.months().is_all() {
797 break;
798 }
799 }
800 Ok(result_vec)
801 }
802}
803
804#[cfg(test)]
805mod tests {
806
807 #[cfg(feature = "io")]
808 macro_rules! test_case {
809 ($fname:expr) => {
810 concat!(env!("CARGO_MANIFEST_DIR"), "/tests/resources/", $fname)
811 };
812 }
813
814 use super::*;
815
816 #[test]
817 fn create_valid_launchd() {
818 let check = Launchd {
819 abandon_process_group: None,
820 debug: None,
821 disabled: None,
822 enable_globbing: None,
823 enable_transactions: None,
824 enable_pressured_exit: None,
825 on_demand: None,
826 service_ipc: None,
827 environment_variables: None,
828 exit_time_out: None,
829 group_name: None,
830 inetd_compatibility: None,
831 init_groups: None,
832 hard_resource_limits: None,
833 keep_alive: None,
834 label: "Label".to_string(),
835 launch_only_once: None,
836 launch_events: None,
837 legacy_timers: None,
838 limit_load_from_hosts: None,
839 limit_load_to_hosts: None,
840 limit_load_to_session_type: None,
841 limit_load_to_hardware: None,
842 limit_load_from_hardware: None,
843 low_priority_io: None,
844 low_priority_background_io: None,
845 hopefully_exits_first: None,
846 hopefully_exits_last: None,
847 mach_services: None,
848 materialize_dataless_files: None,
849 session_create: None,
850 nice: None,
851 process_type: None,
852 program_arguments: None,
853 program: Some("./henk.sh".to_string()),
854 bundle_program: None,
855 queue_directories: None,
856 root_directory: None,
857 run_at_load: None,
858 sockets: None,
859 soft_resource_limits: None,
860 standard_error_path: None,
861 standard_in_path: None,
862 standard_out_path: None,
863 start_calendar_intervals: None,
864 start_interval: None,
865 start_on_mount: None,
866 throttle_interval: None,
867 time_out: None,
868 umask: None,
869 user_name: None,
870 wait_for_debugger: None,
871 watch_paths: None,
872 working_directory: None,
873 };
874 let test = Launchd::new("Label", "./henk.sh");
875 assert!(test.is_ok());
876 assert_eq!(test.unwrap(), check);
877 }
878
879 #[test]
880 fn create_valid_calendar_interval() {
881 let check = CalendarInterval {
882 minute: Some(5),
883 hour: Some(5),
884 day: Some(5),
885 weekday: Some(5),
886 month: Some(5),
887 };
888
889 let test = CalendarInterval::default()
890 .with_day(5)
891 .and_then(|ci| ci.with_minute(5))
892 .and_then(|ci| ci.with_hour(5))
893 .and_then(|ci| ci.with_weekday(5))
894 .and_then(|ci| ci.with_month(5));
895
896 assert!(test.is_ok());
897 assert_eq!(test.unwrap(), check);
898 }
899
900 #[test]
901 fn create_invalid_calendar_interval() {
902 let test = CalendarInterval::default()
903 .with_day(32)
904 .and_then(|ci| ci.with_minute(5))
905 .and_then(|ci| ci.with_hour(5))
906 .and_then(|ci| ci.with_weekday(5))
907 .and_then(|ci| ci.with_month(5));
908 assert!(test.is_err());
909 eprintln!("{}", test.unwrap_err());
910 }
911
912 #[test]
913 #[cfg(feature = "io")]
914 fn load_complex_launch_events_1_plist() {
915 let test = Launchd::from_file(test_case!("launchevents-1.plist")).unwrap();
916
917 match test.launch_events {
918 Some(events) => assert!(events.contains_key("com.apple.distnoted.matching")),
919 _ => panic!("No launch events found"),
920 };
921 }
922
923 #[test]
924 #[cfg(feature = "io")]
925 fn load_complex_launch_events_2_plist() {
926 let check: LaunchEvents = vec![(
927 "com.apple.iokit.matching".to_string(),
928 vec![(
929 "com.apple.device-attach".to_string(),
930 vec![
931 ("IOMatchLaunchStream".to_string(), Value::from(true)),
932 ("idProduct".to_string(), Value::from("*")),
933 ("idVendor".to_string(), Value::from(4176)),
934 ("IOProviderClass".to_string(), Value::from("IOUSBDevice")),
935 ]
936 .into_iter()
937 .collect(),
938 )]
939 .into_iter()
940 .collect(),
941 )]
942 .into_iter()
943 .collect();
944
945 let test = Launchd::from_file(test_case!("launchevents-2.plist")).unwrap();
946
947 match test.launch_events {
948 Some(events) => assert_eq!(events, check),
949 _ => panic!("No launch events found"),
950 };
951 }
952
953 #[test]
954 #[cfg(feature = "io")]
955 fn load_complex_machservices_1_plist() {
956 let check = vec![
957 (
958 "com.apple.private.alloy.accessibility.switchcontrol-idswake".to_string(),
959 MachServiceEntry::from(true),
960 ),
961 (
962 "com.apple.AssistiveControl.startup".to_string(),
963 MachServiceEntry::from(MachServiceOptions::new().reset_at_close()),
964 ),
965 (
966 "com.apple.AssistiveControl.running".to_string(),
967 MachServiceEntry::from(
968 MachServiceOptions::new()
969 .hide_until_check_in()
970 .reset_at_close(),
971 ),
972 ),
973 ]
974 .into_iter()
975 .collect();
976
977 let test = Launchd::from_file(test_case!("machservices-1.plist")).unwrap();
978
979 match test.mach_services {
980 Some(events) => assert_eq!(events, check),
981 _ => panic!("No launch events found"),
982 };
983 }
984}