(*
 * Copyright (C) 2006-2007 XenSource Ltd.
 * Copyright (C) 2008      Citrix Ltd.
 * Author Thomas Gazagnaire <thomas.gazagnaire@citrix.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation; version 2.1 only. with the special
 * exception on linking described in file LICENSE.
 *
 * 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 Lesser General Public License for more details.
 *)

open Stdext
open Printf

let error fmt = Logs.error "general" fmt
let info fmt = Logs.info "general" fmt
let debug fmt = Logs.debug "general" fmt

let access_log_file = ref "/var/log/xenstored-access.log"
let access_log_nb_files = ref 20
let access_log_nb_lines = ref 13215
let activate_access_log = ref true

(* maximal size of the lines in xenstore-acces.log file *)
let line_size = 180

let log_read_ops = ref false
let log_transaction_ops = ref false
let log_special_ops = ref false

type access_type =
	| Coalesce
	| Conflict
	| Commit
	| Newconn
	| Endconn
	| XbOp of Xb.Op.operation

type access =
	{
		fd: out_channel ref;
		counter: int ref;
		write: tid:int -> con:string -> ?data:string -> access_type -> unit;
	}

let string_of_date () =
	let time = Unix.gettimeofday () in
	let tm = Unix.localtime time in
	let msec = time -. (floor time) in
	sprintf "%d%.2d%.2d %.2d:%.2d:%.2d.%.3d" (1900 + tm.Unix.tm_year)
		(tm.Unix.tm_mon + 1)
		tm.Unix.tm_mday
		tm.Unix.tm_hour
		tm.Unix.tm_min
		tm.Unix.tm_sec
		(int_of_float (1000.0 *. msec))

let fill_with_space n s =
	if String.length s < n
	then 
		let r = String.make n ' ' in
		String.blit s 0  r 0 (String.length s);
		r
	else 
		s

let string_of_tid ~con tid =
	if tid = 0
	then fill_with_space 12 (sprintf "%s" con)
	else fill_with_space 12 (sprintf "%s.%i" con tid)

let string_of_access_type = function
	| Coalesce                -> "coalesce "
	| Conflict                -> "conflict "
	| Commit                  -> "commit   "
	| Newconn                 -> "newconn  "
	| Endconn                 -> "endconn  "

	| XbOp op -> match op with
	| Xb.Op.Debug             -> "debug    "

	| Xb.Op.Directory         -> "directory"
	| Xb.Op.Read              -> "read     "
	| Xb.Op.Getperms          -> "getperms "

	| Xb.Op.Watch             -> "watch    "
	| Xb.Op.Unwatch           -> "unwatch  "

	| Xb.Op.Transaction_start -> "t start  "
	| Xb.Op.Transaction_end   -> "t end    "

	| Xb.Op.Introduce         -> "introduce"
	| Xb.Op.Release           -> "release  "
	| Xb.Op.Getdomainpath     -> "getdomain"
	| Xb.Op.Isintroduced      -> "is introduced"
	| Xb.Op.Resume            -> "resume   "
 
	| Xb.Op.Write             -> "write    "
	| Xb.Op.Mkdir             -> "mkdir    "
	| Xb.Op.Rm                -> "rm       "
	| Xb.Op.Setperms          -> "setperms "
	| Xb.Op.Restrict          -> "restrict "
	| Xb.Op.Set_target        -> "settarget"

	| Xb.Op.Error             -> "error    "
	| Xb.Op.Watchevent        -> "w event  "

	| x                       -> Xb.Op.to_string x

let file_exists file =
	try
		Unix.close (Unix.openfile file [Unix.O_RDONLY] 0o644);
		true
	with _ ->
		false

let log_rotate fd =
	let file n = sprintf "%s.%i" !access_log_file n in
	let log_files =
		let rec aux accu n =
			if n >= !access_log_nb_files
			then accu
			else if n = 1 && file_exists !access_log_file
			then aux [!access_log_file,1] 2
			else
				let file = file (n-1) in
				if file_exists file
				then aux ((file,n) :: accu) (n+1)
				else accu
		in
		aux [] 1
	in
	let rec rename = function
		| (f,n) :: t when n < !access_log_nb_files -> 
			Unix.rename f (file n);
			rename t
		| _ -> ()
	in
	rename log_files;
	close_out !fd;
	fd := open_out !access_log_file

let sanitize_data data =
	let data = String.copy data in
	for i = 0 to String.length data - 1
	do
		if data.[i] = '\000' then
			data.[i] <- ' '
	done;
	String.escaped data

let make save_to_disk =
	let fd = ref (open_out_gen [Open_append; Open_creat] 0o644 !access_log_file) in
	let counter = ref 0 in
	{
		fd = fd;
		counter = counter;
		write = 
			if not !activate_access_log || !access_log_nb_files = 0
			then begin fun ~tid ~con ?data _ -> () end
			else fun ~tid ~con ?(data="") access_type ->
				let s = Printf.sprintf "[%s] %s %s %s\n" (string_of_date()) (string_of_tid ~con tid) 
					(string_of_access_type access_type) (sanitize_data data) in
				let s =
					if String.length s > line_size
					then begin
						let s = String.sub s 0 line_size in
						s.[line_size-3] <- '.'; 
						s.[line_size-2] <- '.';
						s.[line_size-1] <- '\n';
						s
					end else
						s
				in
				incr counter;
				output_string !fd s;
				flush !fd;
				if !counter > !access_log_nb_lines 
				then begin 
					log_rotate fd;
					save_to_disk ();
					counter := 0;
				end
	}

let access : (access option) ref = ref None
let init aal save_to_disk =
	activate_access_log := aal;
	access := Some (make save_to_disk)

let write_access_log ~con ~tid ?data access_type = 
        try
	  maybe (fun a -> a.write access_type ~con ~tid ?data) !access
	with _ -> ()

let new_connection = write_access_log Newconn
let end_connection = write_access_log Endconn
let read_coalesce ~tid ~con data =
	if !log_read_ops
	then write_access_log Coalesce ~tid ~con ~data:("read "^data)
let write_coalesce data = write_access_log Coalesce ~data:("write "^data)
let conflict = write_access_log Conflict
let commit = write_access_log Commit

let xb_op ~tid ~con ~ty data =
	let print =
	match ty with
		| Xb.Op.Read | Xb.Op.Directory | Xb.Op.Getperms -> !log_read_ops
		| Xb.Op.Transaction_start | Xb.Op.Transaction_end ->
			false (* transactions are managed below *)
		| Xb.Op.Introduce | Xb.Op.Release | Xb.Op.Getdomainpath | Xb.Op.Isintroduced | Xb.Op.Resume ->
			!log_special_ops
		| _ -> true
	in
		if print 
		then write_access_log ~tid ~con ~data (XbOp ty)

let start_transaction ~tid ~con = 
	if !log_transaction_ops && tid <> 0
	then write_access_log ~tid ~con (XbOp Xb.Op.Transaction_start)

let end_transaction ~tid ~con = 
	if !log_transaction_ops && tid <> 0
	then write_access_log ~tid ~con (XbOp Xb.Op.Transaction_end)

let xb_answer ~tid ~con ~ty data =
	let print = match ty with
		| Xb.Op.Error when data="ENOENT " -> !log_read_ops
		| Xb.Op.Error -> !log_special_ops
		| Xb.Op.Watchevent -> true
		| _ -> false
	in
		if print
		then write_access_log ~tid ~con ~data (XbOp ty)
