AC_MSG_ERROR([You must install the perldoc program])
fi
+dnl Check for mailx (for sending email).
+AC_CHECK_PROG(MAILX,mailx,mailx)
+if test "x$MAILX" = "x"; then
+ AC_MSG_ERROR([You must install the mailx program])
+fi
+
AC_CONFIG_HEADERS([config.h])
AC_CONFIG_FILES([Makefile
daemon/Makefile
putenv "JOBNAME" job.job_name;
(* Create a temporary file containing the shell script fragment. *)
- let script = dir // "script" in
+ let script = dir // "script.sh" in
let chan = open_out script in
fprintf chan "set -e\n"; (* So that jobs exit on error. *)
output_string chan job.job_script.sh_script;
let shell = try getenv "SHELL" with Not_found -> "/bin/sh" in
+ (* Set output to file. *)
+ let output = dir // "output.txt" in
+ let fd = openfile output [O_WRONLY; O_CREAT; O_TRUNC; O_NOCTTY] 0o600 in
+ dup2 fd stdout;
+ dup2 fd stderr;
+ close fd;
+
(* Execute the shell script. *)
(try execvp shell [| shell; "-c"; script |];
with Unix_error (err, fn, _) ->
(* Look up the PID in the running jobs map. *)
let job, dir = IntMap.find pid !running in
running := IntMap.remove pid !running;
- cleanup_job job dir
+ cleanup_job job dir status
)
with Unix_error _ | Not_found -> ()
-and cleanup_job job dir =
+and cleanup_job job dir status =
+ (* If there is a cleanup function, run it. *)
+ (match job.job_cleanup with
+ | None -> ()
+ | Some cleanup ->
+ let code =
+ match status with
+ | WEXITED c -> c
+ | WSIGNALED s | WSTOPPED s -> 1 in
+ let result = {
+ res_job_name = job.job_name;
+ res_code = code;
+ res_tmpdir = dir;
+ res_output = dir // "output.txt"
+ } in
+ try cleanup result
+ with
+ | Failure msg ->
+ Syslog.error "job %s cleanup function failed: %s" job.job_name msg
+ | exn ->
+ Syslog.error "job %s cleanup function exception: %s"
+ job.job_name (Printexc.to_string exn)
+ );
+
(* This should be safe because the path cannot contain shell metachars. *)
let cmd = sprintf "rm -rf '%s'" dir in
ignore (Sys.command cmd)
let package_name = "@PACKAGE_NAME@"
let package_version = "@PACKAGE_VERSION@"
+
+let mailx = "@MAILX@"
val package_name : string
val package_version : string
+
+val mailx : string
$`int:stop_line$, $`int:stop_bol$, $`int:stop_off$,
$`bool:ghost$) >>
+(* Convert 'expr option' to an expression that contains the option inside. *)
+let expr_of_option _loc = function
+ | None -> <:expr< None >>
+ | Some e -> <:expr< Some $e$ >>
+
(* "Lift" an expression, turning it from an expression into an OCaml
* abstract syntax tree in the output. This is pretty obscure.
* http://caml.inria.fr/pub/ml-archives/caml-list/2008/09/591f7c4a8df9295d675a5adcb6802748.en.html
let lift_expr = M.Expr.meta_expr
(* Handle a top level statement. *)
-let rec call_stmt name (_loc, stmt, sh) =
+let rec call_stmt name cleanup (_loc, stmt, sh) =
let name =
match name with
| None -> let name = unique_job_name () in <:expr< $str:name$ >>
| Some name -> name in
+ let cleanup = expr_of_option _loc cleanup in
match stmt with
- | `When e -> when_stmt _loc name e sh
- | `Every p -> every_stmt _loc name p sh
+ | `When e -> when_stmt _loc name cleanup e sh
+ | `Every p -> every_stmt _loc name cleanup p sh
(* Handle a top level "when" statement.
* e -> when expression
* Returns a top level statement (str_item) which when executed just
* adds the statement to a global list.
*)
-and when_stmt _loc name e sh =
+and when_stmt _loc name cleanup e sh =
let loc = expr_of_loc _loc _loc in
let e = lift_expr _loc e in
<:str_item<
open Camlp4.PreCast
- Whenfile.add_when_job $loc$ $name$ $e$ $sh$
+ Whenfile.add_when_job $loc$ $name$ $cleanup$ $e$ $sh$
>>
(* Handle a top level "every" statement. *)
-and every_stmt _loc name period sh =
+and every_stmt _loc name cleanup period sh =
let loc = expr_of_loc _loc _loc in
<:str_item<
open Camlp4.PreCast
- Whenfile.add_every_job $loc$ $name$ $period$ $sh$
+ Whenfile.add_every_job $loc$ $name$ $cleanup$ $period$ $sh$
>>
let () =
| [ e = period_parser -> e ]
];
+ (* Cleanup function. *)
+ cleanup: [
+ [ "cleanup"; f = expr -> f ]
+ ];
+
(* Top level statements. *)
statement: [
[ "when"; e = expr; ":"; sh = expr ->
(* "str_item" is a top level statement in an OCaml program. *)
str_item: LEVEL "top" [
- [ s = statement -> call_stmt None s ]
- | [ "job"; name = expr; s = statement -> call_stmt (Some name) s ]
+ [ s = statement -> call_stmt None None s ]
+ | [ "job"; name = expr;
+ cleanup = OPT cleanup;
+ s = statement ->
+ call_stmt (Some name) cleanup s ]
];
END
sh_script : string;
}
+type result = {
+ res_job_name : string;
+ res_code : int;
+ res_tmpdir : string;
+ res_output : string;
+}
+
+type cleanup = result -> unit
+
type variable =
| T_unit
| T_bool of bool
type job = {
job_loc : Loc.t;
job_name : string;
+ job_cleanup : cleanup option;
job_cond : job_cond;
job_script : shell_script;
}
}
(** A shell script. *)
+type result = {
+ res_job_name : string; (** Job name. *)
+ res_code : int; (** Return code from the script. *)
+ res_tmpdir : string; (** Temporary directory. *)
+ res_output : string; (** Filename of output from job. *)
+}
+(** Result of the run of a job. *)
+
+type cleanup = result -> unit
+(** A cleanup function. *)
+
type variable =
| T_unit
| T_bool of bool
type job = {
job_loc : Camlp4.PreCast.Loc.t;
job_name : string;
+ job_cleanup : cleanup option;
job_cond : job_cond;
job_script : shell_script;
}
let init s = state := s
-let add_when_job _loc name e sh =
+let add_when_job _loc name cleanup e sh =
let e = expr_of_ast _loc e in
- let job = { job_loc = _loc; job_name = name;
+ let job = { job_loc = _loc; job_name = name; job_cleanup = cleanup;
job_cond = When_job e; job_script = sh } in
state := Whenstate.add_job !state job
-let add_every_job _loc name e sh =
- let job = { job_loc = _loc; job_name = name;
+let add_every_job _loc name cleanup e sh =
+ let job = { job_loc = _loc; job_name = name; job_cleanup = cleanup;
job_cond = Every_job e; job_script = sh } in
state := Whenstate.add_job !state job
val get_state : unit -> Whenstate.t
(** Return the updated state. Call this after parsing the file. *)
-val add_when_job : Camlp4.PreCast.Loc.t -> string -> Camlp4.PreCast.Ast.expr -> Whenexpr.shell_script -> unit
+val add_when_job : Camlp4.PreCast.Loc.t -> string -> Whenexpr.cleanup option -> Camlp4.PreCast.Ast.expr -> Whenexpr.shell_script -> unit
(** When a 'when' macro appears as a toplevel statement in an
input file, it causes this function to be called.
[name] is the name of the job.
+ [cleanup] is the optional cleanup function.
+
[expr] is the expression, as an OCaml abstract syntax tree.
[sh] is the shell script fragment (basically location + a big string). *)
-val add_every_job : Camlp4.PreCast.Loc.t -> string -> Whenexpr.periodexpr -> Whenexpr.shell_script -> unit
+val add_every_job : Camlp4.PreCast.Loc.t -> string -> Whenexpr.cleanup option -> Whenexpr.periodexpr -> Whenexpr.shell_script -> unit
(** When an 'every' macro appears as a toplevel statement in an
input file, it causes this function to be called.
[name] is the name of the job.
+ [cleanup] is the optional cleanup function.
+
[periodexpr] is the period, eg. 30 seconds.
[sh] is the shell script fragment. *)
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*)
-open Big_int
open Whenexpr
+open Big_int
+open Printf
+
let set_variable name value =
check_valid_variable_name name;
Whenfile.set_variable name (T_string value)
let set_variable_float name value =
check_valid_variable_name name;
Whenfile.set_variable name (T_float value)
+
+type result = Whenexpr.result
+
+let mailto ?(only_on_failure = false) ?from email result =
+ if result.res_code <> 0 || not only_on_failure then (
+ let subject =
+ sprintf "%s: %s (return code %d)"
+ result.res_job_name
+ (if result.res_code = 0 then "successful" else "FAILED")
+ result.res_code in
+
+ let cmd = sprintf "%s -s %s -a %s"
+ Config.mailx
+ (Filename.quote subject)
+ (Filename.quote result.res_output) in
+
+ let cmd =
+ match from with
+ | None -> cmd
+ | Some from -> sprintf "%s -r %s" cmd from in
+
+ let cmd =
+ sprintf "%s %s </dev/null" cmd (Filename.quote email) in
+
+ if Sys.command cmd <> 0 then
+ failwith "Whentools.mailto: mailx command failed";
+ )
val set_variable_float : string -> float -> unit
(** Set variable (just a wrapper around {!Whenfile.set_variable}). *)
+
+type result = Whenexpr.result
+
+val mailto : ?only_on_failure:bool -> ?from:string -> string -> result -> unit
+(** Cleanup function to send mail. *)
EXTRA_DIST = $(SOURCES) test_load.ml
-SOURCES = t010_load.ml t020_simple.ml t030_jobnames.ml t040_ocaml_jobnames.ml
+SOURCES = \
+ t010_load.ml \
+ t020_simple.ml \
+ t030_jobnames.ml \
+ t040_ocaml_jobnames.ml \
+ t050_cleanups.ml
tests = $(SOURCES:.ml=.cmo)
--- /dev/null
+(* whenjobs
+ * 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.
+ *)
+
+(* Test cleanup functions. *)
+
+job "with cleanup"
+cleanup (Whentools.mailto "you@example.com")
+when true :
+<<
+ # nothing
+>>
Whentools.set_variable "name" "Richard";
Whentools.set_variable_int "counter" 0
+=head3 CLEANUP FUNCTIONS
+
+After a job runs, you can control what happens to its output by
+writing a cleanup function. To write a cleanup function you have to
+name the job (ie. have an explicit C<job> statement). Put C<cleanup ...>
+after the job name like this:
+
+ job "poll source"
+ cleanup (Whentools.mailto "you@example.com")
+ every 10 seconds :
+ <<
+ # ...
+ >>
+
+A number of cleanup functions are available in the library; see below.
+
+You can also write your own cleanup functions (in OCaml). The
+function is passed one argument which is a C<Whentools.result> struct,
+defined below.
+
=head3 WHENTOOLS LIBRARY
+=head4 Functions
+
=over 4
+=item B<Whentools.mailto> [I<~only_on_failure:true>]
+[I<~from:from_address>] I<email_address> I<result>
+
+Send the result of the script by email to the given email address.
+
+If the optional C<~only_on_failure:true> flag is set, then it is only
+sent out if the script failed.
+
+If the optional C<~from> flag is set, then the from address is set
+accordingly. This is sometimes needed when sending mail.
+
+Note the C<result> parameter is passed implicitly by the daemon. You
+do not need to add it.
+
+Here are some examples of using the mailto function:
+
+ job "ex.1"
+ cleanup (Whentools.mailto "you@example.com")
+ every 10 seconds :
+ <<
+ # do something
+ >>
+
+ job "ex.2"
+ cleanup (Whentools.mailto ~only_on_failure:true
+ "you@example.com")
+ every 10 seconds :
+ <<
+ # do something
+ >>
+
+ let from = "me@example.com"
+ let to_addr = "you@example.com"
+
+ job "ex.3"
+ cleanup (Whentools.mailto ~from to_addr)
+ every 10 seconds :
+ <<
+ # do something
+ >>
+
=item B<Whentools.set_variable> I<name> I<string>
Set variable I<name> to the string.
=back
+=head4 Structures
+
+=over 4
+
+=item B<Whentools.result>
+
+This structure is passed to cleanup functions. It has the following
+fields:
+
+ type result = {
+ res_job_name : string; # job name
+ res_code : int; # return code from the shell script
+ res_tmpdir : string; # temporary directory script ran in
+ res_output : string; # filename of stdout/stderr output
+ }
+
+=back
+
=head1 FILES
Requires: ocaml-camlp4-devel
Requires: ocaml-findlib-devel
+# Requires mailx for sending email.
+Requires: /usr/bin/mailx
+
%description
Whenjobs is a powerful but simple cron replacement.