Fix typo in documentation of 'whenjobs --test' option.
[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   ] in
120
121   (* anon_fun normally just collects up the anonymous arguments as
122    * strings, and most modes just use 'args' as a list of strings.
123    * However for `Set and `Test modes we need to record the type of
124    * each argument as well, so we keep that in a separate list
125    * (argtypes).
126    *)
127   let argtypes = ref [] in
128   let anon_fun str = argtypes := (str, !typ) :: !argtypes in
129
130   let usage_msg = "\
131 Whenjobs is a powerful but simple cron replacement.
132 Whenjobs copyright (C) 2012 Red Hat Inc.
133
134 Editing the script:
135
136   whenjobs -e | --edit
137   whenjobs -l | --list
138
139 Get and set variables:
140
141   whenjobs --get variable
142   whenjobs --set variable=value
143
144 Start and stop the per-user daemon:
145
146   whenjobs --daemon-start | --daemon-stop | --daemon-status
147
148 For documentation see the whenjobs(1) man page.
149
150 Options:
151 " in
152
153   Arg.parse argspec anon_fun usage_msg;
154
155   let mode = !mode in
156   let argtypes = List.rev !argtypes in
157   let args = List.map fst argtypes in
158   let nr_args = List.length args in
159   let arg1 = match args with [] -> "" | a::_ -> a in
160
161   (* Depending on the selected mode, perform a different action. *)
162   match mode with
163   | None ->
164     eprintf "whenjobs: no operation selected.\n";
165     suggest_help ();
166     exit 1
167
168   | Some `Edit ->
169     unused_error args "-e";
170     edit_file ()
171
172   | Some `List ->
173     unused_error args "-l";
174     list_file ()
175
176   | Some `Upload ->
177     unused_error args "--upload";
178     upload_file ()
179
180   | Some `Set ->
181     if nr_args = 2 && not (String.contains arg1 '=') then (
182       eprintf "'whenjobs --set variable value' is the old whenjobs <= 0.5 syntax!\n";
183       eprintf "You need to change this to:\n";
184       eprintf "  whenjobs --set variable=value\n";
185       suggest_help ();
186       exit 1
187     );
188     (* Just ignore the case where no variables are defined, to make
189      * it easier to write shell scripts.
190      *)
191     if nr_args > 0 then
192       set_variables argtypes
193
194   | Some `Test ->
195     if nr_args > 0 then
196       test_variables argtypes
197
198   | Some `Get ->
199     if nr_args != 1 then (
200       eprintf "whenjobs --get variable\n";
201       suggest_help ();
202       exit 1
203     );
204     get_variable arg1
205
206   | Some `Variables ->
207     unused_error args "--variables";
208     list_variables ()
209
210   | Some `Daemon_start ->
211     unused_error args "--daemon-start";
212     daemon_start ()
213
214   | Some `Daemon_stop ->
215     unused_error args "--daemon-stop";
216     daemon_stop ()
217
218   | Some `Daemon_restart ->
219     unused_error args "--daemon-restart";
220     daemon_restart ()
221
222   | Some `Daemon_status ->
223     unused_error args "--daemon-status";
224     daemon_status ()
225
226   | Some `Jobs ->
227     unused_error args "--jobs";
228     jobs ()
229
230   | Some `Cancel ->
231     if nr_args != 1 then (
232       eprintf "whenjobs --cancel serial\n";
233       suggest_help ();
234       exit 1
235     );
236     cancel_job arg1
237
238   | Some `Start ->
239     if nr_args != 1 then (
240       eprintf "whenjobs --start jobname\n";
241       eprintf "If 'value' contains spaces, you may need to quote it.\n";
242       suggest_help ();
243       exit 1
244     );
245     start_job arg1
246
247   | Some `Tail ->
248     if nr_args != 1 then (
249       eprintf "whenjobs --tail serial\n";
250       suggest_help ();
251       exit 1
252     );
253     tail arg1
254
255   | Some `JobNames ->
256     unused_error args "--job-names";
257     job_names ()
258
259 and edit_file () =
260   (* If there is no initial file, create an empty one containing the
261    * tutorial.
262    *)
263   let file = get_jobs_filename () in
264   if not (Sys.file_exists file) then
265     create_tutorial file;
266
267   (* Is $EDITOR set?  If not, use a default. *)
268   let editor = try getenv "EDITOR" with Not_found -> "vi" in
269
270   (* Get the (size, MD5) of the file to tell if it changed. *)
271   let file_stamp () =
272     try (lstat file).st_size, Digest.file file
273     with Unix_error (err, fn, _) ->
274       eprintf "whenjobs: %s: %s: %s\n" fn file (error_message err);
275       exit 1
276   in
277   let old_stamp = file_stamp () in
278
279   let cmd = sprintf "%s %s" editor file in
280   if Sys.command cmd != 0 then (
281     eprintf "whenjobs: error editing file (is $EDITOR set correctly?)\n";
282     exit 1
283   );
284
285   let new_stamp = file_stamp () in
286
287   if old_stamp <> new_stamp then
288     upload_file ()
289
290 and list_file () =
291   let file = get_jobs_filename () in
292   if not (Sys.file_exists file) then (
293     eprintf "whenjobs: there is no jobs file, use 'whenjobs -e' to create one\n";
294     exit 1
295   );
296   let chan = open_in file in
297   let rec loop () =
298     printf "%s\n" (input_line chan);
299     loop ()
300   in
301   (try loop () with End_of_file -> ());
302   close_in chan
303
304 and upload_file () =
305   (* Recompile the jobs file. *)
306   let file = get_jobs_filename () in
307   let cmo_file = sprintf "%s/jobs.cmo" jobsdir in
308   let cmd = sprintf "ocamlfind ocamlc -I +camlp4 -I %s -package unix,camlp4.lib -pp 'camlp4o %s/pa_when.cmo' -c %s -o %s"
309     !libdir !libdir file cmo_file in
310   if Sys.command cmd <> 0 then (
311     eprintf "whenjobs: could not compile jobs script, see earlier error messages\n";
312     eprintf "compile command was:\n%s\n" cmd;
313     exit 1
314   );
315
316   (* Test-load the jobs file to ensure it makes sense. *)
317   Whenfile.init Whenstate.empty;
318   (try
319      Dynlink.loadfile cmo_file
320    with
321      Dynlink.Error err ->
322        eprintf "whenjobs: %s\n" (Dynlink.error_message err);
323        (* Since it failed, unlink it. *)
324        (try unlink cmo_file with Unix_error _ -> ());
325        exit 1
326   );
327
328   (* OK now let's tell the daemon to reload it. *)
329   let client = start_client () in
330   (match Whenproto_clnt.When.V1.reload_file client () with
331   | `ok -> ()
332   | `error msg ->
333     eprintf "whenjobs: reload: %s\n" msg;
334     suggest_check_server_logs ();
335     exit 1
336   );
337   stop_client client
338
339 and set_variables argtypes =
340   let vars = List.map (
341     fun (def, typ) ->
342       (* 'def' should have the form "name=value".  The value part may
343        * be missing, but the equals sign is required.
344        *)
345       let i =
346         try String.index def '='
347         with Not_found ->
348           eprintf "whenjobs: set: missing = sign in variable definition\n";
349           suggest_help ();
350           exit 1 in
351       let name = String.sub def 0 i in
352       let value = String.sub def (i+1) (String.length def - (i+1)) in
353       let value = value_of_string value typ in
354       { Whenproto_aux.sv_name = name; sv_value = value }
355   ) argtypes in
356   let vars = Array.of_list vars in
357
358   let client = start_client () in
359   (match Whenproto_clnt.When.V1.set_variables client vars with
360   | `ok -> ()
361   | `error msg ->
362     eprintf "whenjobs: set: %s\n" msg;
363     suggest_check_server_logs ();
364     exit 1
365   );
366   stop_client client
367
368 and test_variables argtypes =
369   let vars = List.map (
370     fun (def, typ) ->
371       (* 'def' should have the form "name=value".  The value part may
372        * be missing, but the equals sign is required.
373        *)
374       let i =
375         try String.index def '='
376         with Not_found ->
377           eprintf "whenjobs: set: missing = sign in variable definition\n";
378           suggest_help ();
379           exit 1 in
380       let name = String.sub def 0 i in
381       let value = String.sub def (i+1) (String.length def - (i+1)) in
382       let value = value_of_string value typ in
383       { Whenproto_aux.sv_name = name; sv_value = value }
384   ) argtypes in
385   let vars = Array.of_list vars in
386
387   let client = start_client () in
388   let jobnames = Whenproto_clnt.When.V1.test_variables client vars in
389   stop_client client;
390
391   Array.iter print_endline jobnames
392
393 and get_variable name =
394   let client = start_client () in
395   let value = Whenproto_clnt.When.V1.get_variable client name in
396   print_endline (string_of_variable value);
397   stop_client client
398
399 and list_variables () =
400   let client = start_client () in
401   let names = Whenproto_clnt.When.V1.get_variable_names client () in
402   Array.iter (
403     fun name ->
404       let value = Whenproto_clnt.When.V1.get_variable client name in
405       printf "%s=%s\n" name (string_of_variable value)
406   ) names;
407   stop_client client
408
409 and daemon_start () =
410   exit (Sys.command "whenjobsd")
411
412 and daemon_stop () =
413   let client = start_client () in
414   (match Whenproto_clnt.When.V1.exit_daemon client () with
415   | `ok -> ()
416   | `error msg ->
417     eprintf "whenjobs: daemon-stop: %s\n" msg;
418     suggest_check_server_logs ();
419     exit 1
420   );
421   stop_client client
422
423 and daemon_restart () =
424   (try
425      let client = start_client_no_exit () in
426      ignore (Whenproto_clnt.When.V1.exit_daemon client ());
427      stop_client client
428    with _ -> ()
429   );
430   sleep 1;
431   daemon_start ()
432
433 and daemon_status () =
434   let r =
435     try
436       let client = start_client_no_exit () in
437       let r = Whenproto_clnt.When.V1.ping_daemon client () in
438       stop_client client;
439       r = `ok
440     with
441       exn -> false in
442   print_endline (if r then "up" else "down")
443
444 and jobs () =
445   let client = start_client () in
446   let jobs = Whenproto_clnt.When.V1.get_jobs client () in
447   stop_client client;
448
449   let cmp { Whenproto_aux.job_name = name1; job_serial = serial1 }
450       { Whenproto_aux.job_name = name2; job_serial = serial2 } =
451     let i = compare name1 name2 in
452     if i <> 0 then i
453     else
454       compare_big_int (big_int_of_string serial1) (big_int_of_string serial2)
455   in
456   Array.sort cmp jobs;
457
458   Array.iter (
459     fun { Whenproto_aux.job_serial = serial; job_name = name;
460           job_tmpdir = tmpdir; job_start_time = time } ->
461       printf "%s %s\n\trunning in: %s\n\tstarted at: %s\n"
462         serial name tmpdir
463         (string_of_time_t ~localtime:true (Int64.to_float time))
464   ) jobs
465
466 and cancel_job serial =
467   let client = start_client () in
468   (match Whenproto_clnt.When.V1.cancel_job client serial with
469   | `ok -> ()
470   | `error msg ->
471     eprintf "whenjobs: cancel-job: %s\n" msg;
472     suggest_check_server_logs ();
473     exit 1
474   );
475   stop_client client
476
477 and start_job name =
478   let client = start_client () in
479   (match Whenproto_clnt.When.V1.start_job client name with
480   | `ok -> ()
481   | `error msg ->
482     eprintf "whenjobs: start-job: %s\n" msg;
483     suggest_check_server_logs ();
484     exit 1
485   );
486   stop_client client
487
488   (* This only works for local.  If we ever make whenjobs work
489    * remotely we'll have to change the implementation to use
490    * the server.
491    *)
492 and tail serial =
493   let client = start_client () in
494   let job = Whenproto_clnt.When.V1.get_job client serial in
495   stop_client client;
496   let cmd =
497     sprintf "tail -f %s/output.txt"
498       (Filename.quote job.Whenproto_aux.job_tmpdir) in
499   exit (Sys.command cmd)
500
501 and job_names () =
502   let client = start_client () in
503   let names = Whenproto_clnt.When.V1.get_job_names client () in
504   stop_client client;
505   Array.iter print_endline names
506
507 and unused_error args op =
508   if args <> [] then (
509     eprintf "whenjobs %s: unused parameters on the command line.\n" op;
510     suggest_help ();
511     exit 1
512   )
513
514 and suggest_help () =
515   eprintf "Use 'whenjobs --help' for a summary of options or read whenjobs(1) man page.\n"
516
517 and suggest_check_server_logs () =
518   eprintf "Look at the server logs (/var/log/cron or /var/log/syslog usually) for\n";
519   eprintf "further information on why this daemon operation failed.\n"
520
521 and get_jobs_filename () =
522   sprintf "%s/jobs.ml" jobsdir
523
524 and create_tutorial file =
525   let chan = open_out file in
526   output_string chan Tutorial.tutorial;
527   close_out chan
528
529 and start_client () =
530   let addr = sprintf "%s/socket" jobsdir in
531   let client =
532     try start_client_no_exit ()
533     with
534     | Unix_error ((ECONNREFUSED|ENOENT), _, _) ->
535       eprintf "whenjobs: error: the daemon ('whenjobsd') is not running\n";
536       eprintf "Use 'whenjobs --daemon-start' to start the daemon.\n";
537       exit 1
538     | Unix_error (err, fn, _) ->
539       eprintf "whenjobs: %s: %s: %s\n" fn addr (error_message err);
540       exit 1 in
541   client
542
543 and start_client_no_exit () =
544   let addr = sprintf "%s/socket" jobsdir in
545   Whenproto_clnt.When.V1.create_client
546     (Rpc_client.Unix addr)
547     Rpc.Tcp (* not TCP, this is the same as SOCK_STREAM *)
548
549 and stop_client client =
550   Rpc_client.shut_down client
551
552 and string_of_variable = function
553   | `unit_t -> ""
554   | `bool_t b -> string_of_bool b
555   | `string_t s -> s
556   | `int_t i -> i (* passed on the wire as a string *)
557   | `float_t f -> string_of_float f
558
559 and value_of_string value = function
560   | `Bool ->
561     (match value with
562     | "true"|"t"|"yes"|"y"|"on"|"1" -> `bool_t true
563     | "false"|"f"|"no"|"n"|"off"|"0" -> `bool_t false
564     | _ ->
565       eprintf "whenjobs: variable does not have a boolean value\n";
566       exit 1
567     )
568   | `String -> `string_t value
569   | `Int ->
570     (try ignore (big_int_of_string value)
571      with Failure _ ->
572        eprintf "whenjobs: variable is not an integer\n";
573        exit 1
574     );
575     `int_t value (* the string is what we pass over the wire *)
576   | `Float ->
577     (try `float_t (float_of_string value)
578      with Failure _ ->
579        eprintf "whenjobs: variable is not a floating point number\n";
580        exit 1
581     )
582   | `Unit ->
583     if value <> "" then (
584       eprintf "whenjobs: unit variables must be empty strings\n";
585       exit 1
586     );
587     `unit_t
588
589 let () =
590   try main ()
591   with
592     (* Pretty print some of the exceptions that main can throw. *)
593   | Rpc.Rpc_server err ->
594     eprintf "whenjobs: rpc error: %s\n" (Rpc.string_of_server_error err);
595     suggest_check_server_logs ();
596     exit 1
597   | Failure msg ->
598     eprintf "whenjobs: error: %s\n" msg;
599     exit 1
600   | Invalid_argument msg ->
601     eprintf "whenjobs: invalid argument: %s\n" msg;
602     exit 1
603   | exn ->
604     eprintf "whenjobs: error: %s\n" (Printexc.to_string exn);
605     exit 1