note
	description: "A Unix process launcher with IO redirection capability."
	legal: "See notice at end of class."
	status: "See notice at end of class."
	date: "$Date$"
	revision: "$Revision$"

class PROCESS_UNIX_PROCESS_MANAGER

inherit
	UNIX_SIGNALS
		rename
			meaning as signal_meaning
		export
			{PROCESS_UNIX_PROCESS_MANAGER} all
			{NONE} is_defined, is_ignored, signal, catch, ignore,
				reset_all_default, reset_default, is_caught,
				c_signal_map, c_signal_name
		end

	PROCESS_UNIX_OS
		rename
			send_signal as unix_send_signal,
			terminate_hard as unix_terminate_hard
		export
			{NONE} all
		end

	OPERATING_ENVIRONMENT
		export
			{NONE} all
		end

	MEMORY
		export
			{NONE} all
		end

	PROCESS_INFO_IMP
		rename
			process_id as current_process_id
		export
			{NONE} all
		end

	RT_DEBUGGER

create {PROCESS_UNIX_OS}
	make

feature {NONE} -- Creation

	make (fname: READABLE_STRING_GENERAL; args: detachable LIST [READABLE_STRING_GENERAL]; a_working_dir: detachable READABLE_STRING_GENERAL)
			-- Create a process object which represents an
			-- independent process that can execute the
			-- program residing in file `fname'
		require
			file_name_exists: fname /= Void
			file_name_not_empty: not fname.is_empty
		do
			if a_working_dir /= Void then
				create working_directory.make_from_string (a_working_dir)
			else
				working_directory := Void
			end
			program_file_name := fname
			set_arguments (args)
			process_id := 0
			set_input_file_name ("")
			set_output_file_name ("")
			set_error_file_name ("")
			set_is_executing (False)
		ensure
			program_file_name_set: program_file_name.same_string (fname)
			input_file_name_empty: attached input_file_name as l_input_fn and then l_input_fn.is_empty
			output_file_name_empty: attached output_file_name as l_output_fn and then l_output_fn.is_empty
			error_file_name_empty: attached error_file_name as l_error_fn and then l_error_fn.is_empty
			input_not_piped: not input_piped
			output_not_piped: not output_piped
			error_not_piped: not error_piped
			no_process_id: process_id = 0
			is_executing_set: not is_executing
		end

feature -- Access

	process_id: INTEGER
			-- Process id of last child process spawned or
			-- 0 if no processes have been spawned.

	status: INTEGER
			-- last reported process status if `is_status_available' is True

	exit_code: INTEGER
			-- Exit code
		do
			if not is_executing and then is_last_wait_successful and then is_status_available then
				Result := exit_code_from_status (status)
			end
		end

	working_directory: detachable PATH
			-- Working directory of process

	arguments_for_exec: detachable ARRAY [READABLE_STRING_GENERAL]
			-- Arguments to be passed to `exec_process'

feature -- Status report

	is_last_wait_successful: BOOLEAN
			-- Is last `wait_for_process' succeeded?

	is_executing: BOOLEAN
			-- Is launched process executing?

	is_stopped: BOOLEAN
			-- Is current process stopped by a signal?
		do
			Result := is_status_available and then signaled_flag_from_status (status)
		end

	is_last_process_spawn_successful: BOOLEAN
			-- Is last process spawn successful?
			-- Check it after invoking `spawn_nowait'.
		do
			Result := process_id /= -1
		end

	is_status_available: BOOLEAN
			-- Is process status available when `wait_for_process' is called last time?	

	is_terminated_by_signal: BOOLEAN
			-- Is process terminated by a signal?
		do
			Result := is_status_available and then signaled_flag_from_status (status)
		end

feature -- Setting

	set_arguments (argument_list: detachable LIST [READABLE_STRING_GENERAL])
			-- Initialize `arguments' from `argument_list'.
		local
			l_arguments: like arguments
		do
			if attached argument_list then
				create l_arguments.make (0)
				across
					argument_list as argument
				loop
					l_arguments.extend (argument.item.twin)
				end
			end
			arguments := l_arguments
		end

	set_close_nonstandard_files (b: BOOLEAN)
			-- Set `close_nonstandard_files' to `b'
		do
			close_nonstandard_files := b
		ensure
			value_set: close_nonstandard_files = b
		end

	set_input_file_name (fname: like input_file_name)
			-- Set `input_file_name' to `fname', which must
			-- be the name of an existing file readable by
			-- the parent process
		do
			input_file_name := fname
			input_piped := fname = Void
		end

	set_output_file_name (fname: like output_file_name)
			-- Set `output_file_name' to `fname', which must
			-- be the name of a file writable by the parent
			-- process.  File is created if it does not exist
			-- and truncated if it does
		do
			output_file_name := fname
			output_piped := fname = Void
		end

	set_error_file_name (fname: like error_file_name)
			-- Set `error_file_name' to `fname', which must
			-- be the name of a file writable by the parent
			-- process.  File is created if it does not exist
			-- and truncated if it does
		do
			error_file_name := fname
			error_piped := fname = Void
			error_same_as_output := False
		ensure
			error_not_output: not error_same_as_output
		end

	set_error_same_as_output
			-- Set `error_same_as_output' to true
		do
			error_same_as_output := True
			error_piped := False
			error_file_name := Void
		ensure
			error_is_output: error_same_as_output
			error_not_piped: not error_piped
			error_not_file: error_file_name = Void
		end

feature {BASE_PROCESS_IMP} -- Process management

	wait_for_process (pid: INTEGER; is_block: BOOLEAN)
			-- Wait for any process specified by process
			-- id `pid' to return status.
			-- Block if `is_block' is True.
			-- Set `is_status_available' to True if process status is available and
			-- if so, set `status' with reported process status.
		local
			l_status: INTEGER
			l_status_available, l_success: BOOLEAN
		do
			unix_waitpid (pid, is_block, $l_status_available, $l_status, $l_success)
			is_last_wait_successful := l_success
			if is_last_wait_successful then
				is_status_available := l_status_available
				status := l_status
				if
					is_status_available and then
				  	(terminate_flag_from_status (status) or signaled_flag_from_status (status))
				 then
					set_is_executing (False)
				else
					set_is_executing (True)
				end
			else
				is_status_available := False
				set_is_executing (False)
			end
		end

	close_pipes
			-- Close opened pipes.
		do
			if attached shared_input_unnamed_pipe as l_in_pipe then
				l_in_pipe.close_write_descriptor
			end
			if attached shared_output_unnamed_pipe as l_out_pipe then
				l_out_pipe.close_read_descriptor
			end
			if attached shared_error_unnamed_pipe as l_err_pipe then
				l_err_pipe.close_read_descriptor
			end
		end

	spawn_nowait (is_control_terminal_enabled: BOOLEAN; envs: detachable HASH_TABLE [READABLE_STRING_GENERAL, READABLE_STRING_GENERAL]; a_new_process_group: BOOLEAN)
			-- Spawn a process and return immediately.
			-- If `is_control_terminal_enabled' is true, attach controlling terminals to spawned process.
			-- Environment variables for new process is stored in `envptr'. If `envptr' is `default_pointer',
			-- new process will inherit all environment variables from its parent process.
			-- If `a_new_process_group' is True, launch process in a new process group.
			-- Check `is_last_process_spawn_successful' after to make sure process has been spawned successfully.
        local
            ee: EXECUTION_ENVIRONMENT
            cur_dir: detachable PATH
            l_debug_state: like debug_state
            l_working_directory: like working_directory
            l_arguments: like arguments_for_exec
        do
            l_arguments := built_argument_list
            arguments_for_exec := l_arguments
            open_files_and_pipes
            create ee
            l_working_directory := working_directory
            if l_working_directory /= Void then
                cur_dir := ee.current_working_path
                ee.change_working_path (l_working_directory)
            end
            l_debug_state := debug_state
            disable_debug

            process_id := fork_process
            inspect process_id
            when -1 then --| Error
                set_debug_state (l_debug_state)
                -- Error ... no fork allowed
                if cur_dir /= Void then
                    ee.change_working_path (cur_dir)
                end
            when 0 then --| Child process
                collection_off
                if a_new_process_group then
                    new_process_group
                    if is_control_terminal_enabled then
                        attach_terminals (process_id)
                    end
                end
                setup_child_process_files
                exec_process (program_file_name, l_arguments, close_nonstandard_files, environment_table_as_pointer (envs))
            else --| Parent process
				set_debug_state (l_debug_state)
                setup_parent_process_files
                arguments_for_exec := Void
                set_is_executing (True)
                if cur_dir /= Void then
                    ee.change_working_path (cur_dir)
                end
            end
        rescue
            if process_id = 0 then
                ;(create {EXCEPTIONS}).die (1)
            end
			set_debug_state (l_debug_state)
        end

	terminate_hard (is_tree: BOOLEAN)
			-- Send a kill signal (SIGKILL) to process(es).
			-- if `is_tree' is True, send signal to whole process group,
			-- otherwise only send signal to current launched process.
		do
			if is_tree then
				unix_terminate_hard (-process_id)
			else
				unix_terminate_hard (process_id)
			end
		end

	read_output_stream (buf_size: INTEGER)
			-- Read at most `buf_size' bytes of data from output pipe.
			-- Set data in `last_output`.
		require
			buf_size_positive: buf_size > 0
		do
			if attached shared_output_unnamed_pipe as l_out_pipe then
				l_out_pipe.read_stream_non_block (buf_size)
				last_output := l_out_pipe.last_string
			else
				last_output := Void
			end
		end

	read_output_to_special (buffer: SPECIAL [NATURAL_8])
			-- Read data from the output stream to `buffer`.
			-- Maximum number if bytes to read is `buffer.count`.
			-- Update `buffer.count` with actually read bytes.
		do
			if attached shared_output_unnamed_pipe as p then
				p.read_to_special (buffer)
				has_output_stream_error := not p.last_read_successful
			else
				has_output_stream_error := True
					-- Update `buffer.count` with actual bytes read.
				buffer.wipe_out
				check
					buffer_is_empty: buffer.count = 0
				end
			end
		end

	read_error_stream (buf_size: INTEGER)
			-- Read at most `buf_size' bytes of data from error pipe.
			-- Set data in `last_error'.
		require
			buf_size_positive: buf_size > 0
		do
			if not error_same_as_output and then attached shared_error_unnamed_pipe as l_err_pipe then
				l_err_pipe.read_stream_non_block (buf_size)
				last_error := l_err_pipe.last_string
			else
				last_error := Void
			end
		end

	read_error_to_special (buffer: SPECIAL [NATURAL_8])
			-- Read data from the error stream to `buffer`.
			-- Maximum number if bytes to read is `buffer.count`.
			-- Update `buffer.count` with actually read bytes.
		do
			if attached shared_error_unnamed_pipe as p then
				p.read_to_special (buffer)
				has_error_stream_error := not p.last_read_successful
			else
				has_error_stream_error := True
					-- Update `buffer.count` with actual bytes read.
				buffer.wipe_out
				check
					buffer_is_empty: buffer.count = 0
				end
			end
		end

	put_string (s: STRING)
			-- Put `s' into input pipe of process.
		require
			s_not_void: s /= Void
		do
			if is_executing and then attached shared_input_unnamed_pipe as l_in_pipe then
				l_in_pipe.put_string (s)
				has_write_error := not l_in_pipe.last_write_successful
			else
				has_write_error := True
			end
		end

	has_write_error: BOOLEAN
			-- Has last write operation to `shared_input_unnamed_pipe' ended with an error?

	has_output_stream_error: BOOLEAN
			-- Has last read operation from `shared_output_unnamed_pipe' ended with an error?

	has_error_stream_error: BOOLEAN
			-- Has last read operation from `shared_error_unnamed_pipe' ended with an error?

	last_output: detachable STRING
			-- Last read data from output pipe

	last_error: detachable STRING
			-- Last read data from error pipe

feature {NONE} -- Properties

	program_file_name: READABLE_STRING_GENERAL;
			-- Name of file containing program which will be
			-- executed when process is spawned

	arguments: detachable ARRAYED_LIST [READABLE_STRING_GENERAL];
			-- Arguments to passed to process when it is spawned,
			-- not including argument 0 (which is conventionally
			-- the name of the program).  If Void or if count
			-- is zero, then no arguments are passed

	close_nonstandard_files: BOOLEAN;
			-- Should nonstandard files (files other than
			-- standard input, standard output and standard
			-- error) be closed in the spawned process?

	input_file_name: detachable READABLE_STRING_GENERAL;
			-- Name of file to be used as standard input in
			-- spawned process if `input_descriptor' is not a
			-- valid descriptor and `input_piped' is false.
			-- A Void value leaves standard input same as
			-- parent's and an empty string closes standard input

	output_file_name: detachable READABLE_STRING_GENERAL;
			-- Name of file to be used as standard output in
			-- spawned process if `output_descriptor' is not a
			-- valid descriptor and `output_piped' is false.
			-- A Void value leaves standard output same as
			-- parent's and an empty string closes standard output

	error_file_name: detachable READABLE_STRING_GENERAL;
			-- Name of file to be used as standard error in
			-- spawned process if `error_descriptor' is not a
			-- valid descriptor and `error_piped' is false.
			-- A Void value leaves standard error same as
			-- parent's and an empty string closes standard error

	input_piped: BOOLEAN;
			-- Should standard input for spawned process
			-- come from a pipe connected to parent,
			-- instead of from a file descriptor or named file?

	output_piped: BOOLEAN;
			-- Should standard output for spawned process go to a
			-- pipe connected to parent, instead of to a
			-- file descriptor or named file?

	error_piped: BOOLEAN;
			-- Should standard error for spawned process go to a
			-- pipe connected to parent, instead of to a
			-- file descriptor or named file?

	error_same_as_output: BOOLEAN;
			-- Should standard error for spawned process be
			-- the same as standard output (i.e., identical
			-- file descriptor)?		

feature {NONE} -- Implementation

	built_argument_list: attached like arguments_for_exec
			-- Build argument list for `exec_process' call
			-- and put it in `arguments_for_exec'.
			-- Make `process_name' argument 0 and append
			-- `arguments' as the rest of the arguments
		local
			k: INTEGER
		do
			if attached arguments as a then
				create Result.make_filled (program_file_name, 1, a.count + 1)
				across
					a as argument
				from
					k := 2
				loop
					Result.put (argument.item, k)
					k := k + 1
				end
			else
				create Result.make_filled (program_file_name, 1, 1)
			end
		end

	open_files_and_pipes
		local
			pipe_fac: UNIX_PIPE_FACTORY
			l_input_fn: like input_file_name
			l_output_fn: like output_file_name
			l_error_fn: like error_file_name
		do
			create pipe_fac
				-- Open file or pipe for input.
			shared_input_unnamed_pipe := Void
			in_file := Void
			if input_piped then
				shared_input_unnamed_pipe := pipe_fac.new_unnamed_pipe
			else
				l_input_fn := input_file_name
				if l_input_fn /= Void and then not l_input_fn.is_empty then
					create in_file.make_open_read (l_input_fn)
				end
			end
				-- Open file or pipe for output.
			shared_output_unnamed_pipe := Void
			out_file := Void
			if output_piped then
				shared_output_unnamed_pipe := pipe_fac.new_unnamed_pipe
			else
				l_output_fn := output_file_name
				if l_output_fn /= Void and then not l_output_fn.is_empty then
					create out_file.make_open_append (l_output_fn)
				end
			end

				-- Open file or pipe for error.
			shared_error_unnamed_pipe := Void
			err_file := Void
			if not error_same_as_output then
				if error_piped then
					shared_error_unnamed_pipe := pipe_fac.new_unnamed_pipe
				else
					l_error_fn := error_file_name
					if l_error_fn /= Void and then not l_error_fn.is_empty then
						create err_file.make_open_append (l_error_fn)
					end
				end
			end
		end

	setup_parent_process_files
		do
			if attached shared_input_unnamed_pipe as l_in_pipe then
				l_in_pipe.close_read_descriptor
			elseif attached in_file as l_in_file then
				l_in_file.close
			end

			if attached shared_output_unnamed_pipe as l_out_pipe then
				l_out_pipe.close_write_descriptor
			elseif attached out_file as l_out_file then
				l_out_file.close
			end

			if not error_same_as_output then
				if attached shared_error_unnamed_pipe as l_err_pipe then
					l_err_pipe.close_write_descriptor
				elseif attached err_file as l_err_file then
					l_err_file.close
				end
			end
		end

	setup_child_process_files
		do
			if attached shared_input_unnamed_pipe as l_in_pipe then
				move_desc (l_in_pipe.read_descriptor, stdin_descriptor)
				l_in_pipe.close_write_descriptor
			elseif attached in_file as l_in_file then
				move_desc (l_in_file.descriptor, stdin_descriptor)
			end

			if attached shared_output_unnamed_pipe as l_out_pipe then
				move_desc (l_out_pipe.write_descriptor, stdout_descriptor)
				l_out_pipe.close_read_descriptor
			elseif attached out_file as l_out_file then
				move_desc (l_out_file.descriptor, stdout_descriptor)
			end

			if error_same_as_output then
				duplicate_file_descriptor (stdout_descriptor, stderr_descriptor)
			else
				if attached shared_error_unnamed_pipe as l_err_pipe then
					move_desc (l_err_pipe.write_descriptor, stderr_descriptor)
					l_err_pipe.close_read_descriptor
				elseif attached err_file as l_err_file then
					move_desc (l_err_file.descriptor, stderr_descriptor)
				end
			end
		end

	move_desc (source, dest: INTEGER)
			-- If descriptor `source' is different than
			-- descriptor `dest', duplicate `source' onto `dest'
			-- and close `source'
		do
			if source /= dest then
				duplicate_file_descriptor (source, dest)
				close_file_descriptor (source)
			end
		end

feature {NONE} -- Input-output

	in_file: detachable RAW_FILE
			-- File to be used by child process for standard input
			-- when it comes from a file

	out_file: detachable RAW_FILE
			-- File to be used by child process for standard output
			-- when it goes to a file

	err_file: detachable RAW_FILE
			-- File to be used by child process for standard error
			-- when it goes to a file

	Stdin_descriptor: INTEGER = 0
			-- File descriptor for standard input

	Stdout_descriptor: INTEGER = 1
			-- File descriptor for standard output

	Stderr_descriptor: INTEGER = 2
			-- File descriptor for standard error

feature {NONE} -- Access

	environment_table_as_pointer (a_envs: detachable HASH_TABLE [READABLE_STRING_GENERAL, READABLE_STRING_GENERAL]): POINTER
			-- {POINTER} representation of `environment_variable_table'.
			-- Return `default_pointer' if `environment_variable_table' is Void or empty.
			--| Not that memory will be leaked if not use for spawning the process.
		local
			l_ptr: MANAGED_POINTER
			l_natstr: NATIVE_STRING
			l_cstr_ptr: POINTER
			i, nb: INTEGER
			l_str: STRING_32
		do
			if a_envs /= Void and then not a_envs.is_empty then
					-- Estimate the number of environment variables that will be stored.
				across
					a_envs as c
				loop
					if attached c.key and then attached c.item then
						nb := nb + 1
					end
				end
					-- We allocate `Result', then use a MANAGED_POINTER to fill its content.
				Result := Result.memory_alloc ((nb + 1) * {PLATFORM}.pointer_bytes)
				create l_ptr.share_from_pointer (Result, (nb + 1) * {PLATFORM}.pointer_bytes)
				across
					a_envs as c
				loop
					if
						attached c.key as l_key and then
						attached c.item as l_value
					then
						create l_str.make (l_key.count + l_value.count + 1)
						l_str.append_string_general (l_key)
						l_str.append_character ('=')
						l_str.append_string_general (l_value)

						create l_natstr.make (l_str)

						l_cstr_ptr := l_cstr_ptr.memory_alloc (l_natstr.bytes_count + 1)
						l_cstr_ptr.memory_copy (l_natstr.item, l_natstr.bytes_count + 1)

						l_ptr.put_pointer (l_cstr_ptr, i * {PLATFORM}.pointer_bytes)

						i := i + 1
					end
				end
				l_ptr.put_pointer (default_pointer, i * {PLATFORM}.pointer_bytes)
			end
		end

	shared_input_unnamed_pipe: detachable UNIX_UNNAMED_PIPE
			-- Pipe used to redirect input of process

	shared_output_unnamed_pipe: detachable UNIX_UNNAMED_PIPE
			-- Pipe used to redirect output of process

	shared_error_unnamed_pipe: detachable UNIX_UNNAMED_PIPE
			-- Pipe used to redirect error of process

	exit_code_from_status (a_status: INTEGER): INTEGER
			-- Exit code evaluated from status returned by process
		external
			"C inline use %"eif_process.h%""
		alias
			"WEXITSTATUS($a_status)"
		end

	terminate_flag_from_status (a_status: INTEGER): BOOLEAN
			-- Returns true if the child terminated normally.
		external
			"C inline use %"eif_process.h%""
		alias
			"WIFEXITED($a_status)"
		end

	signaled_flag_from_status (a_status: INTEGER): BOOLEAN
			-- Returns true if the child process was terminated by a signal.
		external
			"C inline use %"eif_process.h%""
		alias
			"WIFSIGNALED($a_status)"
		end

	stopped_flag_from_status (a_status: INTEGER): BOOLEAN
			-- Returns true if the child process was stopped by delivery of a signal.
		external
			"C inline use %"eif_process.h%""
		alias
			"WIFSTOPPED($a_status)"
		end

	continued_flag_from_status (a_status: INTEGER): BOOLEAN
			-- Returns true if the child process was stopped by delivery of a signal.
		external
			"C inline use %"eif_process.h%""
		alias
			"WIFSTOPPED($a_status)"
		end

	set_is_executing (b: BOOLEAN)
			-- Set `is_executing' with `b'.
		do
			is_executing := b
		ensure
			is_executing_set: is_executing = b
		end

invariant
	input_piped_no_file: input_piped implies input_file_name = Void
	output_piped_no_file: output_piped implies output_file_name = Void
	error_piped_no_file: error_piped implies error_file_name = Void

	input_named_no_pipe: input_file_name /= Void implies not input_piped
	output_named_no_pipe: output_file_name /= Void implies not output_piped
	error_named_no_pipe: error_file_name /= Void implies not error_piped

	valid_stdin_descriptor: valid_file_descriptor (Stdin_descriptor)
	valid_stdout_descriptor: valid_file_descriptor (Stdout_descriptor)
	valid_stderr_descriptor: valid_file_descriptor (Stderr_descriptor)

note
	copyright: "Copyright (c) 1984-2017, Eiffel Software and others"
	license:   "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
	source: "[
			Eiffel Software
			5949 Hollister Ave., Goleta, CA 93117 USA
			Telephone 805-685-1006, Fax 805-685-6869
			Website http://www.eiffel.com
			Customer support http://support.eiffel.com
		]"

end