Renumber the jobs file so that it can be reloaded in native code.
[whenjobs.git] / tools / whenjobs.ml
1 (* whenjobs daemon
2  * Copyright (C) 2012 Red Hat Inc.
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 2 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 along
15  * with this program; if not, write to the Free Software Foundation, Inc.,
16  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17  *)
18
19 open Big_int
20 open Unix
21 open Printf
22
23 open Whenutils
24
25 (* Ensures that Whentools module is linked to the whenjobs tool. *)
26 let _ = Whentools.set_variable
27
28 let libdir = ref Libdir.libdir
29
30 let jobsdir =
31   (* Running the program as root is a mistake.  It must be run as a
32    * non-root user.
33    *)
34   let euid = geteuid () in
35   if euid = 0 then (
36     eprintf "whenjobs: this program must not be run as root\n";
37     exit 1
38   );
39
40   (* $HOME must be defined and must exist and be a directory and must be
41    * owned by the current user.
42    *)
43   let home =
44     try getenv "HOME"
45     with Not_found ->
46       eprintf "whenjobs: $HOME environment variable must be defined\n";
47       exit 1 in
48
49   let stat =
50     try lstat home
51     with Unix_error (err, fn, _) ->
52       eprintf "whenjobs: %s: %s ($HOME): %s\n" fn home (error_message err);
53       exit 1 in
54   if stat.st_kind != S_DIR then (
55     eprintf "whenjobs: %s ($HOME): not a directory\n" home;
56     exit 1
57   );
58
59   if stat.st_uid != euid then (
60     eprintf "whenjobs: %s ($HOME): not owned by the current user (uid %d)\n"
61       home euid;
62     exit 1
63   );
64
65   (* Make the $HOME/.whenjobs directory if it doesn't exist. *)
66   let jobsdir = sprintf "%s/.whenjobs" home in
67   (try mkdir jobsdir 0o700 with Unix_error _ -> ());
68
69   jobsdir
70
71 let rec main () =
72   (* Parse the command line arguments. *)
73   let mode = ref None in
74   let typ = ref `String in
75
76   let set_mode m () = mode := Some m in
77
78   let set_type t =
79     typ :=
80       match t with
81       | "bool"|"boolean" -> `Bool
82       | "string" -> `String
83       | "int" -> `Int
84       | "float"|"double" -> `Float
85       | "unit" -> `Unit
86       | _ ->
87         eprintf "whenjobs: --type: unknown type (%s)\n" t;
88         exit 1
89   in
90
91   let display_version () =
92     printf "%s %s\n" Config.package_name Config.package_version;
93     exit 0
94   in
95
96   let argspec = Arg.align [
97     "--cancel", Arg.Unit (set_mode `Cancel), " Cancel a job";
98     "--daemon-start", Arg.Unit (set_mode `Daemon_start), " Start the daemon";
99     "--daemon-status", Arg.Unit (set_mode `Daemon_status), " Display the status of the daemon";
100     "--daemon-stop", Arg.Unit (set_mode `Daemon_stop), " Stop the daemon";
101     "--daemon-restart", Arg.Unit (set_mode `Daemon_restart), " Restart the daemon";
102     "-e", Arg.Unit (set_mode `Edit), " Edit and upload the script";
103     "--edit", Arg.Unit (set_mode `Edit), " Edit and upload the script";
104     "--get", Arg.Unit (set_mode `Get), "var Display the variable";
105     "--job-names", Arg.Unit (set_mode `JobNames), " List names of loaded jobs";
106     "--jobs", Arg.Unit (set_mode `Jobs), " List running jobs";
107     "-l", Arg.Unit (set_mode `List), " List the script";
108     "--list", Arg.Unit (set_mode `List), " List the script";
109     "--lib", Arg.Set_string libdir, "dir Specify directory that contains pa_when.cmo";
110     "--set", Arg.Unit (set_mode `Set), " Set the variable";
111     "--start", Arg.Unit (set_mode `Start), "name Start a job manually";
112     "--tail", Arg.Unit (set_mode `Tail), "serial Tail job output";
113     "--test", Arg.Unit (set_mode `Test), " Test the effect of setting variables";
114     "--type", Arg.String set_type, "bool|int|float|string|.. Set the variable type";
115     "--upload", Arg.Unit (set_mode `Upload), " Upload the script";
116     "--variables", Arg.Unit (set_mode `Variables), " Display all variables and values";
117     "-V", Arg.Unit display_version, " Display version number and exit";
118     "--version", Arg.Unit display_version, " Display version number and exit";
119     "--whisper", Arg.Unit (set_mode `Whisper), " Set the variable, quietly";
120   ] in
121
122   (* anon_fun normally just collects up the anonymous arguments as
123    * strings, and most modes just use 'args' as a list of strings.
124    * However for `Set, `Test and `Whisper modes we need to record the
125    * type of each argument as well, so we keep that in a separate list
126    * (argtypes).
127    *)
128   let argtypes = ref [] in
129   let anon_fun str = argtypes := (str, !typ) :: !argtypes in
130
131   let usage_msg = "\
132 Whenjobs is a powerful but simple cron replacement.
133 Whenjobs copyright (C) 2012 Red Hat Inc.
134
135 Editing the script:
136
137   whenjobs -e | --edit
138   whenjobs -l | --list
139
140 Get and set variables:
141
142   whenjobs --get variable
143   whenjobs --set variable=value
144
145 Start and stop the per-user daemon:
146
147   whenjobs --daemon-start | --daemon-stop | --daemon-status
148
149 For documentation see the whenjobs(1) man page.
150
151 Options:
152 " in
153
154   Arg.parse argspec anon_fun usage_msg;
155
156   let mode = !mode in
157   let argtypes = List.rev !argtypes in
158   let args = List.map fst argtypes in
159   let nr_args = List.length args in
160   let arg1 = match args with [] -> "" | a::_ -> a in
161
162   (* Depending on the selected mode, perform a different action. *)
163   match mode with
164   | None ->
165     eprintf "whenjobs: no operation selected.\n";
166     suggest_help ();
167     exit 1
168
169   | Some `Edit ->
170     unused_error args "-e";
171     edit_file ()
172
173   | Some `List ->
174     unused_error args "-l";
175     list_file ()
176
177   | Some `Upload ->
178     unused_error args "--upload";
179     upload_file ()
180
181   | Some `Set ->
182     if nr_args = 2 && not (String.contains arg1 '=') then (
183       eprintf "'whenjobs --set variable value' is the old whenjobs <= 0.5 syntax!\n";
184       eprintf "You need to change this to:\n";
185       eprintf "  whenjobs --set variable=value\n";
186       suggest_help ();
187       exit 1
188     );
189     (* Just ignore the case where no variables are defined, to make
190      * it easier to write shell scripts.
191      *)
192     if nr_args > 0 then
193       set_variables argtypes
194
195   | Some `Test ->
196     if nr_args > 0 then
197       test_variables argtypes
198
199   | Some `Whisper ->
200     if nr_args > 0 then
201       whisper_variables argtypes
202
203   | Some `Get ->
204     if nr_args != 1 then (
205       eprintf "whenjobs --get variable\n";
206       suggest_help ();
207       exit 1
208     );
209     get_variable arg1
210
211   | Some `Variables ->
212     unused_error args "--variables";
213     list_variables ()
214
215   | Some `Daemon_start ->
216     unused_error args "--daemon-start";
217     daemon_start ()
218
219   | Some `Daemon_stop ->
220     unused_error args "--daemon-stop";
221     daemon_stop ()
222
223   | Some `Daemon_restart ->
224     unused_error args "--daemon-restart";
225     daemon_restart ()
226
227   | Some `Daemon_status ->
228     unused_error args "--daemon-status";
229     daemon_status ()
230
231   | Some `Jobs ->
232     unused_error args "--jobs";
233     jobs ()
234
235   | Some `Cancel ->
236     if nr_args != 1 then (
237       eprintf "whenjobs --cancel serial\n";
238       suggest_help ();
239       exit 1
240     );
241     cancel_job arg1
242
243   | Some `Start ->
244     if nr_args != 1 then (
245       eprintf "whenjobs --start jobname\n";
246       eprintf "If 'value' contains spaces, you may need to quote it.\n";
247       suggest_help ();
248       exit 1
249     );
250     start_job arg1
251
252   | Some `Tail ->
253     if nr_args != 1 then (
254       eprintf "whenjobs --tail serial\n";
255       suggest_help ();
256       exit 1
257     );
258     tail arg1
259
260   | Some `JobNames ->
261     unused_error args "--job-names";
262     job_names ()
263
264 and edit_file () =
265   (* If there is no initial file, create an empty one containing the
266    * tutorial.
267    *)
268   let file = get_jobs_filename () in
269   if not (Sys.file_exists file) then
270     create_tutorial file;
271
272   (* Is $EDITOR set?  If not, use a default. *)
273   let editor = try getenv "EDITOR" with Not_found -> "vi" in
274
275   (* Get the (size, MD5) of the file to tell if it changed. *)
276   let file_stamp () =
277     try (lstat file).st_size, Digest.file file
278     with Unix_error (err, fn, _) ->
279       eprintf "whenjobs: %s: %s: %s\n" fn file (error_message err);
280       exit 1
281   in
282   let old_stamp = file_stamp () in
283
284   let cmd = sprintf "%s %s" editor file in
285   if Sys.command cmd != 0 then (
286     eprintf "whenjobs: error editing file (is $EDITOR set correctly?)\n";
287     exit 1
288   );
289
290   let new_stamp = file_stamp () in
291
292   if old_stamp <> new_stamp then
293     upload_file ()
294
295 and list_file () =
296   let file = get_jobs_filename () in
297   if not (Sys.file_exists file) then (
298     eprintf "whenjobs: there is no jobs file, use 'whenjobs -e' to create one\n";
299     exit 1
300   );
301   let chan = open_in file in
302   let rec loop () =
303     printf "%s\n" (input_line chan);
304     loop ()
305   in
306   (try loop () with End_of_file -> ());
307   close_in chan
308
309 and upload_file () =
310   let suffix = if not Config.have_ocamlopt then "cmo" else "cmx" in
311
312   (* Recompile the jobs file(s). *)
313   let files = get_multijobs_filenames () in
314
315   (* Choose a random name for the output file.  time_t is convenient.
316    * See: https://sympa-roc.inria.fr/wws/arc/caml-list/2012-03/msg00276.html?checked_cas=2
317    *)
318   let t = Int64.of_float (time ()) in
319
320   (* Compilation step. *)
321   List.iter (
322     fun file ->
323       let cmd =
324         if not Config.have_ocamlopt then
325           (* bytecode *)
326           sprintf "%s c -for-pack Jobs__%Ld -I +camlp4 -I %s -package unix,camlp4.lib -pp 'camlp4o %s/pa_when.cmo' -c %s"
327             Config.ocamlfind t !libdir !libdir file
328         else
329           (* native code *)
330           sprintf "%s opt -for-pack Jobs__%Ld -I +camlp4 -I %s -package unix,camlp4.lib -pp 'camlp4o %s/pa_when.cmo' -c %s"
331             Config.ocamlfind t !libdir !libdir file in
332       if Sys.command cmd <> 0 then (
333         eprintf "whenjobs: %s: could not compile jobs script, see earlier errors\n"
334           file;
335         eprintf "compile command was:\n%s\n" cmd;
336         exit 1
337       )
338   ) files;
339
340   (* Pack into a single file. *)
341   let filename = sprintf "%s/jobs__%Ld.%s" jobsdir t suffix in
342   let cmd =
343     let objects = List.map (
344       fun file ->
345         let base = Filename.chop_extension file in
346         base ^ if not Config.have_ocamlopt then ".cmo" else ".cmx"
347     ) files in
348     sprintf "%s %s -pack -o %s %s"
349       Config.ocamlfind
350       (if not Config.have_ocamlopt then "c" else "opt")
351       filename (String.concat " " objects) in
352   if Sys.command cmd <> 0 then (
353     eprintf "whenjobs: could not pack jobs script, see earlier errors\n";
354     eprintf "compile command was:\n%s\n" cmd;
355     exit 1
356   );
357
358   (* For native code only, write a *.cmxs file. *)
359   let filename =
360     if Config.have_ocamlopt then (
361       let cmd = sprintf "%s opt -shared -linkall %s -o %ss"
362         Config.ocamlfind filename filename in
363       if Sys.command cmd <> 0 then (
364         eprintf "whenjobs: could not convert to *.cmxs, see earlier errors\n";
365         eprintf "compile command was:\n%s\n" cmd;
366         exit 1
367       );
368       filename ^ "s" (* .cmx -> .cmxs *)
369     )
370     else filename in
371
372   (* Test-load the jobs files to ensure they make sense. *)
373   Whenfile.init Whenstate.empty;
374   (try Dynlink.loadfile filename
375    with
376      Dynlink.Error err ->
377        eprintf "whenjobs: dynlink: %s\n" (Dynlink.error_message err);
378        (* Since it failed, unlink the compiled file. *)
379        (try unlink filename with Unix_error _ -> ());
380        exit 1
381   );
382
383   (* OK now let's tell the daemon to reload it. *)
384   let client = start_client () in
385   (match Whenproto_clnt.When.V1.reload_file client () with
386   | `ok -> ()
387   | `error msg ->
388     eprintf "whenjobs: reload: %s\n" msg;
389     suggest_check_server_logs ();
390     exit 1
391   );
392   stop_client client
393
394 and set_variables argtypes =
395   let vars = List.map (
396     fun (def, typ) ->
397       (* 'def' should have the form "name=value".  The value part may
398        * be missing, but the equals sign is required.
399        *)
400       let i =
401         try String.index def '='
402         with Not_found ->
403           eprintf "whenjobs: set: missing = sign in variable definition\n";
404           suggest_help ();
405           exit 1 in
406       let name = String.sub def 0 i in
407       let value = String.sub def (i+1) (String.length def - (i+1)) in
408       let value = value_of_string value typ in
409       { Whenproto_aux.sv_name = name; sv_value = value }
410   ) argtypes in
411   let vars = Array.of_list vars in
412
413   let client = start_client () in
414   (match Whenproto_clnt.When.V1.set_variables client vars with
415   | `ok -> ()
416   | `error msg ->
417     eprintf "whenjobs: set: %s\n" msg;
418     suggest_check_server_logs ();
419     exit 1
420   );
421   stop_client client
422
423 and test_variables argtypes =
424   let vars = List.map (
425     fun (def, typ) ->
426       (* 'def' should have the form "name=value".  The value part may
427        * be missing, but the equals sign is required.
428        *)
429       let i =
430         try String.index def '='
431         with Not_found ->
432           eprintf "whenjobs: test: missing = sign in variable definition\n";
433           suggest_help ();
434           exit 1 in
435       let name = String.sub def 0 i in
436       let value = String.sub def (i+1) (String.length def - (i+1)) in
437       let value = value_of_string value typ in
438       { Whenproto_aux.sv_name = name; sv_value = value }
439   ) argtypes in
440   let vars = Array.of_list vars in
441
442   let client = start_client () in
443   let jobnames = Whenproto_clnt.When.V1.test_variables client vars in
444   stop_client client;
445
446   Array.iter print_endline jobnames
447
448 and whisper_variables argtypes =
449   let vars = List.map (
450     fun (def, typ) ->
451       (* 'def' should have the form "name=value".  The value part may
452        * be missing, but the equals sign is required.
453        *)
454       let i =
455         try String.index def '='
456         with Not_found ->
457           eprintf "whenjobs: whisper: missing = sign in variable definition\n";
458           suggest_help ();
459           exit 1 in
460       let name = String.sub def 0 i in
461       let value = String.sub def (i+1) (String.length def - (i+1)) in
462       let value = value_of_string value typ in
463       { Whenproto_aux.sv_name = name; sv_value = value }
464   ) argtypes in
465   let vars = Array.of_list vars in
466
467   let client = start_client () in
468   (match Whenproto_clnt.When.V1.whisper_variables client vars with
469   | `ok -> ()
470   | `error msg ->
471     eprintf "whenjobs: whisper: %s\n" msg;
472     suggest_check_server_logs ();
473     exit 1
474   );
475   stop_client client
476
477 and get_variable name =
478   let client = start_client () in
479   let value = Whenproto_clnt.When.V1.get_variable client name in
480   print_endline (string_of_variable value);
481   stop_client client
482
483 and list_variables () =
484   let client = start_client () in
485   let names = Whenproto_clnt.When.V1.get_variable_names client () in
486   Array.iter (
487     fun name ->
488       let value = Whenproto_clnt.When.V1.get_variable client name in
489       printf "%s=%s\n" name (string_of_variable value)
490   ) names;
491   stop_client client
492
493 and daemon_start () =
494   exit (Sys.command "whenjobsd")
495
496 and daemon_stop () =
497   let client = start_client () in
498   (match Whenproto_clnt.When.V1.exit_daemon client () with
499   | `ok -> ()
500   | `error msg ->
501     eprintf "whenjobs: daemon-stop: %s\n" msg;
502     suggest_check_server_logs ();
503     exit 1
504   );
505   stop_client client
506
507 and daemon_restart () =
508   (try
509      let client = start_client_no_exit () in
510      ignore (Whenproto_clnt.When.V1.exit_daemon client ());
511      stop_client client
512    with _ -> ()
513   );
514   sleep 1;
515   daemon_start ()
516
517 and daemon_status () =
518   let r =
519     try
520       let client = start_client_no_exit () in
521       let r = Whenproto_clnt.When.V1.ping_daemon client () in
522       stop_client client;
523       r = `ok
524     with
525       exn -> false in
526   print_endline (if r then "up" else "down")
527
528 and jobs () =
529   let client = start_client () in
530   let jobs = Whenproto_clnt.When.V1.get_jobs client () in
531   stop_client client;
532
533   let cmp { Whenproto_aux.job_name = name1; job_serial = serial1 }
534       { Whenproto_aux.job_name = name2; job_serial = serial2 } =
535     let i = compare name1 name2 in
536     if i <> 0 then i
537     else
538       compare_big_int (big_int_of_string serial1) (big_int_of_string serial2)
539   in
540   Array.sort cmp jobs;
541
542   Array.iter (
543     fun { Whenproto_aux.job_serial = serial; job_name = name;
544           job_tmpdir = tmpdir; job_start_time = time } ->
545       printf "%s %s\n\trunning in: %s\n\tstarted at: %s\n"
546         serial name tmpdir
547         (string_of_time_t ~localtime:true (Int64.to_float time))
548   ) jobs
549
550 and cancel_job serial =
551   let client = start_client () in
552   (match Whenproto_clnt.When.V1.cancel_job client serial with
553   | `ok -> ()
554   | `error msg ->
555     eprintf "whenjobs: cancel-job: %s\n" msg;
556     suggest_check_server_logs ();
557     exit 1
558   );
559   stop_client client
560
561 and start_job name =
562   let client = start_client () in
563   (match Whenproto_clnt.When.V1.start_job client name with
564   | `ok -> ()
565   | `error msg ->
566     eprintf "whenjobs: start-job: %s\n" msg;
567     suggest_check_server_logs ();
568     exit 1
569   );
570   stop_client client
571
572   (* This only works for local.  If we ever make whenjobs work
573    * remotely we'll have to change the implementation to use
574    * the server.
575    *)
576 and tail serial =
577   let client = start_client () in
578   let job = Whenproto_clnt.When.V1.get_job client serial in
579   stop_client client;
580   let cmd =
581     sprintf "tail -f %s/output.txt"
582       (Filename.quote job.Whenproto_aux.job_tmpdir) in
583   exit (Sys.command cmd)
584
585 and job_names () =
586   let client = start_client () in
587   let names = Whenproto_clnt.When.V1.get_job_names client () in
588   stop_client client;
589   Array.iter print_endline names
590
591 and unused_error args op =
592   if args <> [] then (
593     eprintf "whenjobs %s: unused parameters on the command line.\n" op;
594     suggest_help ();
595     exit 1
596   )
597
598 and suggest_help () =
599   eprintf "Use 'whenjobs --help' for a summary of options or read whenjobs(1) man page.\n"
600
601 and suggest_check_server_logs () =
602   eprintf "Look at the server logs (/var/log/cron or /var/log/syslog usually) for\n";
603   eprintf "further information on why this daemon operation failed.\n"
604
605 and get_jobs_filename () =
606   sprintf "%s/jobs.ml" jobsdir
607
608 and get_multijobs_filenames () =
609   (* Get dir/*.ml *)
610   let files = Array.to_list (Sys.readdir jobsdir) in
611   let files = List.filter (fun file -> string_endswith file ".ml") files in
612   let files = List.map (fun file -> jobsdir // file) files in
613   List.sort compare files
614
615 and create_tutorial file =
616   let chan = open_out file in
617   output_string chan Tutorial.tutorial;
618   close_out chan
619
620 and start_client () =
621   let addr = sprintf "%s/socket" jobsdir in
622   let client =
623     try start_client_no_exit ()
624     with
625     | Unix_error ((ECONNREFUSED|ENOENT), _, _) ->
626       eprintf "whenjobs: error: the daemon ('whenjobsd') is not running\n";
627       eprintf "Use 'whenjobs --daemon-start' to start the daemon.\n";
628       exit 1
629     | Unix_error (err, fn, _) ->
630       eprintf "whenjobs: %s: %s: %s\n" fn addr (error_message err);
631       exit 1 in
632   client
633
634 and start_client_no_exit () =
635   let addr = sprintf "%s/socket" jobsdir in
636   Whenproto_clnt.When.V1.create_client
637     (Rpc_client.Unix addr)
638     Rpc.Tcp (* not TCP, this is the same as SOCK_STREAM *)
639
640 and stop_client client =
641   Rpc_client.shut_down client
642
643 and string_of_variable = function
644   | `unit_t -> ""
645   | `bool_t b -> string_of_bool b
646   | `string_t s -> s
647   | `int_t i -> i (* passed on the wire as a string *)
648   | `float_t f -> string_of_float f
649
650 and value_of_string value = function
651   | `Bool ->
652     (match value with
653     | "true"|"t"|"yes"|"y"|"on"|"1" -> `bool_t true
654     | "false"|"f"|"no"|"n"|"off"|"0" -> `bool_t false
655     | _ ->
656       eprintf "whenjobs: variable does not have a boolean value\n";
657       exit 1
658     )
659   | `String -> `string_t value
660   | `Int ->
661     (try ignore (big_int_of_string value)
662      with Failure _ ->
663        eprintf "whenjobs: variable is not an integer\n";
664        exit 1
665     );
666     `int_t value (* the string is what we pass over the wire *)
667   | `Float ->
668     (try `float_t (float_of_string value)
669      with Failure _ ->
670        eprintf "whenjobs: variable is not a floating point number\n";
671        exit 1
672     )
673   | `Unit ->
674     if value <> "" then (
675       eprintf "whenjobs: unit variables must be empty strings\n";
676       exit 1
677     );
678     `unit_t
679
680 let () =
681   try main ()
682   with
683     (* Pretty print some of the exceptions that main can throw. *)
684   | Rpc.Rpc_server err ->
685     eprintf "whenjobs: rpc error: %s\n" (Rpc.string_of_server_error err);
686     suggest_check_server_logs ();
687     exit 1
688   | Failure msg ->
689     eprintf "whenjobs: error: %s\n" msg;
690     exit 1
691   | Invalid_argument msg ->
692     eprintf "whenjobs: invalid argument: %s\n" msg;
693     exit 1
694   | exn ->
695     eprintf "whenjobs: error: %s\n" (Printexc.to_string exn);
696     exit 1