(*****************************************************************************

  Liquidsoap, a programmable stream generator.
  Copyright 2003-2024 Savonet team

  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, fully stated in the COPYING
  file at the root of the liquidsoap distribution.

  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

 *****************************************************************************)

(** Asynchronous process handler. This module wraps around [Unix.open_process_full]
  * and [Unix.select] to manipulate external processes. Its API can be used in two
  * different ways:
  *  - Provide callbacks for reading and writing to the process' standard outputs.
  *    These callbacks are executed when [Unix.select] returns their respective
  *    file descriptors as available for writing/reading. Thus, read and write
  *    inside these callbacks should not be blocking although [Unix.select]
  *    documentation stipulates that "large" writes may still be blocking.
  *  - Use direct, blocking, read/write on the process' standard output.
  *    in this case, reading and writing may be blocking, following the
  *    usual [Unix.write] and [Unix.read] semantics.
  * The API abstracts the file descriptors and provides writing/reading
  * callbacks instead. Please note that there is no way to detect when the
  * process has exited unless reaching a zero-length read in its standard
  * output or error. Also, the default [on_stdout] and [on_stderr] callbacks
  * do not read from these. Therefore it is important to read all data coming
  * from at least one of these two outputs in order to make sure that the module
  * properly detects when the process has exited. *)

(** A process. *)
type t

(** Type for continuation decision. *)
type continuation =
  [ `Continue
  | `Stop
    (** triggers an asynchronous process stop, closing the process' stdin
             and waiting for termination *)
  | `Kill  (** kill the process immediately *)
  | `Delay of float  (** wait for a given amount of seconds *)
  | `Reschedule of Tutils.priority
    (** Update the process' priority and continue processing. *) ]

(** A call back. *)
type 'a callback = 'a -> continuation

(** Function for reading data. *)
type pull = Bytes.t -> int -> int -> int

(** Function for writing data. *)
type push = Bytes.t -> int -> int -> int

type status = [ `Exception of exn | `Status of Unix.process_status ]

(** Trying to performed an operation on a stopped process. *)
exception Finished

(** Create a process handler. Decisions returned by the callbacks are applied
   synchronously, i.e. when returning [`Stop], the process' stdin is closed
   immediately after the callback has returned. The process is restarted when
   the function [on_stop] returns [true]. *)
val run :
  ?priority:Tutils.priority ->
  ?env:string array ->
  ?on_start:push callback ->
  ?on_stdin:push callback ->
  ?on_stdout:pull callback ->
  ?on_stderr:pull callback ->
  ?on_stop:(status -> bool) ->
  ?log:(string -> unit) ->
  string ->
  t

(** Change the process' asynchronous task priority. Useful when switching from
    blocking read to non-blocking read. *)
val set_priority : t -> Tutils.priority -> unit

(** Asynchronous stop. The process' stdin will be closed some time in the
   future. *)
val stop : t -> unit

(** Asynchronous kill. The process will be killed some time in the future. *)
val kill : t -> unit

(** Whether the process was stopped. *)
val stopped : t -> bool

(** Write bytes recursively. *)
val really_write : ?offset:int -> ?length:int -> bytes -> push -> unit

(** Synchronous (blocking) write on the process' stdin. Raises [Finished] if the
   process has been stopped/killed. *)
val on_stdin : t -> (push -> 'a) -> 'a

(** Synchronous (blocking) read on the process' stdout. Raises [Finished] if the
   process has been stopped/killed. *)
val on_stdout : t -> (pull -> 'a) -> 'a

(** Synchronous (blocking) read on the process' stdout. Raises [Finished] if the
   process has been stopped/killed. *)
val on_stderr : t -> (pull -> 'a) -> 'a
