whenjobs initial version.
[whenjobs.git] / tools / whenjobs.ml
diff --git a/tools/whenjobs.ml b/tools/whenjobs.ml
new file mode 100644 (file)
index 0000000..dade6c2
--- /dev/null
@@ -0,0 +1,394 @@
+(* whenjobs daemon
+ * Copyright (C) 2012 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *)
+
+open Big_int
+open Unix
+open Printf
+
+let libdir = ref Libdir.libdir
+
+let jobsdir =
+  (* Running the program as root is a mistake.  It must be run as a
+   * non-root user.
+   *)
+  let euid = geteuid () in
+  if euid = 0 then (
+    eprintf "whenjobs: this program must not be run as root\n";
+    exit 1
+  );
+
+  (* $HOME must be defined and must exist and be a directory and must be
+   * owned by the current user.
+   *)
+  let home =
+    try getenv "HOME"
+    with Not_found ->
+      eprintf "whenjobs: $HOME environment variable must be defined\n";
+      exit 1 in
+
+  let stat =
+    try lstat home
+    with Unix_error (err, fn, _) ->
+      eprintf "whenjobs: %s: %s ($HOME): %s\n" fn home (error_message err);
+      exit 1 in
+  if stat.st_kind != S_DIR then (
+    eprintf "whenjobs: %s ($HOME): not a directory\n" home;
+    exit 1
+  );
+
+  if stat.st_uid != euid then (
+    eprintf "whenjobs: %s ($HOME): not owned by the current user (uid %d)\n"
+      home euid;
+    exit 1
+  );
+
+  (* Make the $HOME/.whenjobs directory if it doesn't exist. *)
+  let jobsdir = sprintf "%s/.whenjobs" home in
+  (try mkdir jobsdir 0o700 with Unix_error _ -> ());
+
+  jobsdir
+
+let rec main () =
+  (* Parse the command line arguments. *)
+  let mode = ref None in
+  let typ = ref "string" in
+
+  let set_mode m () = mode := Some m in
+
+  let display_version () =
+    printf "%s %s\n" Config.package_name Config.package_version;
+    exit 0
+  in
+
+  let argspec = Arg.align [
+    "--daemon-start", Arg.Unit (set_mode `Daemon_start), " Start the daemon";
+    "--daemon-status", Arg.Unit (set_mode `Daemon_status), " Display the status of the daemon";
+    "--daemon-stop", Arg.Unit (set_mode `Daemon_stop), " Stop the daemon";
+    "--daemon-restart", Arg.Unit (set_mode `Daemon_restart), " Restart the daemon";
+    "-e", Arg.Unit (set_mode `Edit), " Edit and upload the script";
+    "--edit", Arg.Unit (set_mode `Edit), " Edit and upload the script";
+    "--get", Arg.Unit (set_mode `Get), " Display the variable";
+    "-l", Arg.Unit (set_mode `List), " List the script";
+    "--list", Arg.Unit (set_mode `List), " List the script";
+    "--lib", Arg.Set_string libdir, "dir Specify directory that contains pa_when.cmo";
+    "--set", Arg.Unit (set_mode `Set), " Set the variable";
+    "--type", Arg.Set_string typ, "bool|int|float|string Set the variable type";
+    "--upload", Arg.Unit (set_mode `Upload), " Upload the script";
+    "--variables", Arg.Unit (set_mode `Variables), " Display all variables and values";
+    "-V", Arg.Unit display_version, " Display version number and exit";
+    "--version", Arg.Unit display_version, " Display version number and exit";
+  ] in
+
+  let args = ref [] in
+  let anon_fun str = args := str :: !args in
+
+  let usage_msg = "\
+Whenjobs is a powerful but simple cron replacement.
+Whenjobs copyright (C) 2012 Red Hat Inc.
+
+Editing the script:
+
+  whenjobs -e | --edit
+  whenjobs -l | --list
+
+Get and set variables:
+
+  whenjobs --get variable
+  whenjobs --set variable value
+
+Start and stop the per-user daemon:
+
+  whenjobs --daemon-start | --daemon-stop | --daemon-status
+
+For documentation see the whenjobs(1) man page.
+
+Options:
+" in
+
+  Arg.parse argspec anon_fun usage_msg;
+
+  let mode = !mode in
+  let args = List.rev !args in
+
+  let typ = match !typ with
+    | "bool"|"boolean" -> `Bool
+    | "string" -> `String
+    | "int" -> `Int
+    | "float"|"double" -> `Float
+    | t ->
+      eprintf "whenjobs: --type: unknown type (%s)\n" t;
+      exit 1 in
+
+  (* Depending on the selected mode, perform a different action. *)
+  match mode with
+  | None ->
+    eprintf "whenjobs: no operation selected.\n";
+    suggest_help ();
+    exit 1
+
+  | Some `Edit ->
+    unused_error args "-e";
+    edit_file ()
+
+  | Some `List ->
+    unused_error args "-l";
+    list_file ()
+
+  | Some `Upload ->
+    unused_error args "--upload";
+    upload_file ()
+
+  | Some `Set ->
+    if List.length args != 2 then (
+      eprintf "whenjobs --set variable value\n";
+      eprintf "If 'value' contains spaces, you may need to quote it.\n";
+      suggest_help ();
+      exit 1
+    );
+    set_variable (List.hd args) (List.hd (List.tl args)) typ
+
+  | Some `Get ->
+    if List.length args != 1 then (
+      eprintf "whenjobs --get variable\n";
+      suggest_help ();
+      exit 1
+    );
+    get_variable (List.hd args)
+
+  | Some `Variables ->
+    unused_error args "--variables";
+    list_variables ()
+
+  | Some `Daemon_start ->
+    unused_error args "--daemon-start";
+    daemon_start ()
+
+  | Some `Daemon_stop ->
+    unused_error args "--daemon-stop";
+    daemon_stop ()
+
+  | Some `Daemon_restart ->
+    unused_error args "--daemon-restart";
+    daemon_restart ()
+
+  | Some `Daemon_status ->
+    unused_error args "--daemon-status";
+    daemon_status ()
+
+and edit_file () =
+  (* If there is no initial file, create an empty one containing the
+   * tutorial.
+   *)
+  let file = get_jobs_filename () in
+  if not (Sys.file_exists file) then
+    create_tutorial file;
+
+  (* Is $EDITOR set?  If not, use a default. *)
+  let editor = try getenv "EDITOR" with Not_found -> "vi" in
+
+  (* Get the (size, MD5) of the file to tell if it changed. *)
+  let file_stamp () =
+    try (lstat file).st_size, Digest.file file
+    with Unix_error (err, fn, _) ->
+      eprintf "whenjobs: %s: %s: %s\n" fn file (error_message err);
+      exit 1
+  in
+  let old_stamp = file_stamp () in
+
+  let cmd = sprintf "%s %s" editor file in
+  if Sys.command cmd != 0 then (
+    eprintf "whenjobs: error editing file (is $EDITOR set correctly?)\n";
+    exit 1
+  );
+
+  let new_stamp = file_stamp () in
+
+  if old_stamp <> new_stamp then
+    upload_file ()
+
+and list_file () =
+  let file = get_jobs_filename () in
+  if not (Sys.file_exists file) then (
+    eprintf "whenjobs: there is no jobs file, use 'whenjobs -e' to create one\n";
+    exit 1
+  );
+  let chan = open_in file in
+  let rec loop () =
+    printf "%s\n" (input_line chan);
+    loop ()
+  in
+  (try loop () with End_of_file -> ());
+  close_in chan
+
+and upload_file () =
+  (* Recompile the jobs file. *)
+  let file = get_jobs_filename () in
+  let cmo_file = sprintf "%s/jobs.cmo" jobsdir in
+  let cmd = sprintf "ocamlfind ocamlc -I +camlp4 -I %s -package unix,camlp4.lib -pp 'camlp4o %s/pa_when.cmo' -c %s -o %s"
+    !libdir !libdir file cmo_file in
+  if Sys.command cmd <> 0 then (
+    eprintf "whenjobs: could not compile jobs script, see earlier error messages\n";
+    eprintf "compile command was:\n%s\n" cmd;
+    exit 1
+  );
+
+  (* Test-load the jobs file to ensure it makes sense. *)
+  Whenfile.init ();
+  (try
+     Dynlink.loadfile cmo_file
+   with
+     Dynlink.Error err ->
+       eprintf "whenjobs: %s\n" (Dynlink.error_message err);
+       (* Since it failed, unlink it. *)
+       (try unlink cmo_file with Unix_error _ -> ());
+       exit 1
+  );
+
+  (* OK now let's tell the daemon to reload it. *)
+  let client = start_client () in
+  (match Whenproto_clnt.When.V1.reload_file client () with
+  | `ok -> ()
+  | `error msg ->
+    eprintf "whenjobs: reload: %s\n" msg;
+    suggest_check_server_logs ();
+    exit 1
+  );
+  stop_client client
+
+and set_variable name value typ =
+  let value = match typ with
+    | `Bool ->
+      (match value with
+      | "true"|"t"|"yes"|"y"|"on"|"1" -> `bool_t true
+      | "false"|"f"|"no"|"n"|"off"|"0" -> `bool_t false
+      | _ ->
+        eprintf "whenjobs: variable does not have a boolean value\n";
+        exit 1
+      )
+    | `String -> `string_t value
+    | `Int ->
+      (try ignore (big_int_of_string value)
+       with Failure _ ->
+         eprintf "whenjobs: variable is not an integer\n";
+         exit 1
+      );
+      `int_t value (* the string is what we pass over the wire *)
+    | `Float ->
+      (try `float_t (float_of_string value)
+       with Failure _ ->
+         eprintf "whenjobs: variable is not a floating point number\n";
+         exit 1
+      ) in
+
+  let client = start_client () in
+  Whenproto_clnt.When.V1.set_variable client (name, value);
+  stop_client client
+
+and get_variable name =
+  let client = start_client () in
+  let value = Whenproto_clnt.When.V1.get_variable client name in
+  print_endline (string_of_variable value);
+  stop_client client
+
+and list_variables () =
+  let client = start_client () in
+  let names = Whenproto_clnt.When.V1.get_variable_names client () in
+  Array.iter (
+    fun name ->
+      let value = Whenproto_clnt.When.V1.get_variable client name in
+      printf "%s=%s\n" name (string_of_variable value)
+  ) names;
+  stop_client client
+
+and daemon_start () =
+  assert false
+
+and daemon_stop () =
+  assert false
+
+and daemon_restart () =
+  assert false
+
+and daemon_status () =
+  assert false
+
+and unused_error args op =
+  if args <> [] then (
+    eprintf "whenjobs %s: unused parameters on the command line.\n" op;
+    suggest_help ();
+    exit 1
+  )
+
+and suggest_help () =
+  eprintf "Use 'whenjobs --help' for a summary of options or read whenjobs(1) man page.\n"
+
+and suggest_check_server_logs () =
+  eprintf "Look at the server logs (/var/log/cron or /var/log/syslog usually) for\n";
+  eprintf "further information on why this daemon operation failed.\n"
+
+and get_jobs_filename () =
+  sprintf "%s/jobs.ml" jobsdir
+
+and create_tutorial file =
+  let chan = open_out file in
+  output_string chan Tutorial.tutorial;
+  close_out chan
+
+and start_client () =
+  let addr = sprintf "%s/socket" jobsdir in
+  let client =
+    try
+      Whenproto_clnt.When.V1.create_client
+        (Rpc_client.Unix addr)
+        Rpc.Tcp (* not TCP, this is the same as SOCK_STREAM *)
+    with
+    | Unix_error ((ECONNREFUSED|ENOENT), _, _) ->
+      eprintf "whenjobs: error: the daemon ('whenjobsd') is not running\n";
+      eprintf "Use 'whenjobs --daemon-start' to start the daemon.\n";
+      exit 1
+    | Unix_error (err, fn, _) ->
+      eprintf "whenjobs: %s: %s: %s\n" fn addr (error_message err);
+      exit 1 in
+  client
+
+and stop_client client =
+  Rpc_client.shut_down client
+
+and string_of_variable = function
+  | `bool_t b -> string_of_bool b
+  | `string_t s -> s
+  | `int_t i -> i (* passed on the wire as a string *)
+  | `float_t f -> string_of_float f
+
+let () =
+  try main ()
+  with
+    (* Pretty print some of the exceptions that main can throw. *)
+  | Rpc.Rpc_server err ->
+    eprintf "whenjobs: rpc error: %s\n" (Rpc.string_of_server_error err);
+    suggest_check_server_logs ();
+    exit 1
+  | Failure msg ->
+    eprintf "whenjobs: error: %s\n" msg;
+    exit 1
+  | Invalid_argument msg ->
+    eprintf "whenjobs: invalid argument: %s\n" msg;
+    exit 1
+  | exn ->
+    eprintf "whenjobs: error: %s\n" (Printexc.to_string exn);
+    exit 1