// -*- c-basic-offset: 4; tab-width: 8; indent-tabs-mode: t -*-
// vim:set sts=4 ts=8:

// Copyright (c) 2001-2007 International Computer Science Institute
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software")
// to deal in the Software without restriction, subject to the conditions
// listed in the XORP LICENSE file. These conditions include: you must
// preserve this copyright notice, and you cannot mention the copyright
// holders in advertising related to the Software without their permission.
// The Software is provided WITHOUT ANY WARRANTY, EXPRESS OR IMPLIED. This
// notice is a summary of the XORP LICENSE file; the license in that file is
// legally binding.

// $XORP: xorp/libxorp/run_command.cc,v 1.31 2007/02/16 22:46:22 pavlin Exp $

#include "libxorp_module.h"

#include "libxorp/xorp.h"
#include "libxorp/debug.h"
#include "libxorp/xlog.h"
#include "libxorp/eventloop.hh"
#include "libxorp/exceptions.hh"
#include "libxorp/xorpfd.hh"
#include "libxorp/asyncio.hh"
#include "libxorp/popen.hh"
#ifdef HOST_OS_WINDOWS
#include "libxorp/win_io.h"
#endif

#include <map>

#ifdef HAVE_PATHS_H
#include <paths.h>
#endif

#ifdef HAVE_SYS_WAIT_H
#include <sys/wait.h>
#endif

#include "run_command.hh"


#ifdef HOST_OS_WINDOWS

#ifndef _PATH_BSHELL
#define _PATH_BSHELL "C:\\MSYS\\bin\\sh.exe"
#endif

#define	fileno(stream) (_get_osfhandle(_fileno(stream)))

#else // ! HOST_OS_WINDOWS

#ifndef _PATH_BSHELL
#define _PATH_BSHELL "/bin/sh"
#endif

#endif // ! HOST_OS_WINDOWS

static map<pid_t, RunCommandBase *> pid2command;

#ifndef HOST_OS_WINDOWS
static void
child_handler(int signo)
{
    XLOG_ASSERT(signo == SIGCHLD);

    //
    // XXX: Wait for any child process.
    // If we are executing any child process outside of the RunProcess
    // mechanism, then the waitpid() here may create a wait() blocking
    // problem for those processes. If this ever becomes an issue, then
    // we should try non-blocking waitpid() for each pid in the
    // pid2command map.
    //
    do {
	pid_t pid = 0;
	int wait_status = 0;
	map<pid_t, RunCommandBase *>::iterator iter;

	pid = waitpid(-1, &wait_status, WUNTRACED | WNOHANG);
	debug_msg("pid=%d, wait status=%d\n", XORP_INT_CAST(pid), wait_status);
	if (pid <= 0)
	    return;	// XXX: no more child processes

	XLOG_ASSERT(pid > 0);
	popen2_mark_as_closed(pid, wait_status);
	iter = pid2command.find(pid);
	if (iter == pid2command.end()) {
	    // XXX: we don't know anything about the exited process
	    continue;
	}

	RunCommandBase* run_command = iter->second;
	run_command->wait_status_changed(wait_status);
    } while (true);
}
#endif // ! HOST_OS_WINDOWS

static string
get_path_bshell()
{
#ifndef HOST_OS_WINDOWS
    return (string(_PATH_BSHELL));

#else // HOST_OS_WINDOWS
    //
    // Use the DOS style path to the user's shell specified in the
    // environment if available; otherwise, fall back to hard-coded default.
    //
    static string path_bshell;
    if (path_bshell.empty()) {
	char* g = getenv("SHELL");
	if (g != NULL)
	    path_bshell = string(g);
	else
	    path_bshell = string(_PATH_BSHELL);
    }
    return (path_bshell);
#endif // HOST_OS_WINDOWS
}

RunCommand::RunCommand(EventLoop&			eventloop,
		       const string&			command,
		       const list<string>&		argument_list,
		       RunCommand::OutputCallback	stdout_cb,
		       RunCommand::OutputCallback	stderr_cb,
		       RunCommand::DoneCallback		done_cb,
		       bool				redirect_stderr_to_stdout)
    : RunCommandBase(eventloop, command, command),
      _stdout_cb(stdout_cb),
      _stderr_cb(stderr_cb),
      _done_cb(done_cb),
      _redirect_stderr_to_stdout(redirect_stderr_to_stdout)
{
    set_argument_list(argument_list);
}

RunShellCommand::RunShellCommand(EventLoop&		eventloop,
				 const string&		command,
				 const string&		argument_string,
				 RunShellCommand::OutputCallback stdout_cb,
				 RunShellCommand::OutputCallback stderr_cb,
				 RunShellCommand::DoneCallback	 done_cb)
    : RunCommandBase(eventloop, get_path_bshell(), command),
      _stdout_cb(stdout_cb),
      _stderr_cb(stderr_cb),
      _done_cb(done_cb)
{
    list<string> l;

    string final_command_argument_string = command + " " + argument_string;

    l.push_back("-c");
    l.push_back(final_command_argument_string);

    set_argument_list(l);
}

RunCommandBase::RunCommandBase(EventLoop&		eventloop,
			       const string&		command,
			       const string&		real_command_name)
    : _eventloop(eventloop),
      _command(command),
      _real_command_name(real_command_name),
      _stdout_file_reader(NULL),
      _stderr_file_reader(NULL),
      _stdout_stream(NULL),
      _stderr_stream(NULL),
      _last_stdout_offset(0),
      _last_stderr_offset(0),
      _pid(0),
      _is_error(false),
      _is_running(false),
      _command_is_exited(false),
      _command_is_signal_terminated(false),
      _command_is_coredumped(false),
      _command_is_stopped(false),
      _command_exit_status(0),
      _command_term_signal(0),
      _command_stop_signal(0),
      _stdout_eof_received(false),
      _stderr_eof_received(false)
{
    memset(_stdout_buffer, 0, BUF_SIZE);
    memset(_stderr_buffer, 0, BUF_SIZE);

    _done_timer = _eventloop.new_timer(callback(this, &RunCommandBase::done));
}

RunCommandBase::~RunCommandBase()
{
    cleanup();
}

void
RunCommandBase::cleanup()
{
    terminate_with_prejudice();
    close_output();
    // Remove the entry from the pid map and perform other cleanup
    if (_pid != 0) {
	pid2command.erase(_pid);
	_pid = 0;
    }
    _done_timer.unschedule();
    _is_running = false;
    unblock_child_signals();
}

int
RunCommandBase::block_child_signals()
{
#ifndef HOST_OS_WINDOWS
    //
    // Block SIGCHLD signal in current signal mask
    //
    int r;
    sigset_t sigchld_sigset;

    r = sigemptyset(&sigchld_sigset);
    XLOG_ASSERT(r >= 0);
    r = sigaddset(&sigchld_sigset, SIGCHLD);
    XLOG_ASSERT(r >= 0);

    if (sigprocmask(SIG_BLOCK, &sigchld_sigset, NULL) < 0) {
	XLOG_ERROR("Failed to block SIGCHLD in current signal mask: %s",
		   strerror(errno));
	return (XORP_ERROR);
    }
#endif // ! HOST_OS_WINDOWS

    return (XORP_OK);
}

int
RunCommandBase::unblock_child_signals()
{
#ifndef HOST_OS_WINDOWS
    //
    // Unblock SIGCHLD signal in current signal mask
    //
    int r;
    sigset_t sigchld_sigset;

    r = sigemptyset(&sigchld_sigset);
    XLOG_ASSERT(r >= 0);
    r = sigaddset(&sigchld_sigset, SIGCHLD);
    XLOG_ASSERT(r >= 0);

    if (sigprocmask(SIG_UNBLOCK, &sigchld_sigset, NULL) < 0) {
	XLOG_ERROR("Failed to unblock SIGCHLD in current signal mask: %s",
		   strerror(errno));
	return (XORP_ERROR);
    }
#endif // ! HOST_OS_WINDOWS

    return (XORP_OK);
}

int
RunCommandBase::execute()
{
    string error_msg;

    if (_is_running)
	return (XORP_OK);	// XXX: already running

    // Create a single string with the command name and the arguments
    string final_command = _command;
    list<string>::const_iterator iter;
    for (iter = _argument_list.begin(); iter != _argument_list.end(); ++iter) {
	final_command += " ";
	final_command += *iter;
    }

    //
    // Save the current execution ID, and set the new execution ID
    //
    _exec_id.save_current_exec_id();
    if (_exec_id.set_effective_exec_id(error_msg) != XORP_OK) {
	XLOG_ERROR("Failed to set effective execution ID: %s",
		   error_msg.c_str());
	_exec_id.restore_saved_exec_id(error_msg);
	return (XORP_ERROR);
    }

#ifndef HOST_OS_WINDOWS
    signal(SIGCHLD, child_handler);
#endif

    //
    // Temporary block the child signals
    //
    block_child_signals();

    //
    // Run the command
    //
    _pid = popen2(_command, _argument_list, _stdout_stream, _stderr_stream,
		  redirect_stderr_to_stdout());
    if (_stdout_stream == NULL) {
	XLOG_ERROR("Failed to execute command \"%s\"", final_command.c_str());
	cleanup();
	_exec_id.restore_saved_exec_id(error_msg);
	return (XORP_ERROR);
    }
    // Insert the new process to the map of processes
    XLOG_ASSERT(pid2command.find(_pid) == pid2command.end());
    pid2command[_pid] = this;

#ifdef HOST_OS_WINDOWS
    // We need to obtain the handle directly from the popen code because
    // the handle returned by CreateProcess() has the privileges we need.
    _ph = pgethandle(_pid);

    // If the handle is invalid, the process failed to start.
    if (_ph == INVALID_HANDLE_VALUE) {
	XLOG_ERROR("Failed to execute command \"%s\"", final_command.c_str());
	cleanup();
	_exec_id.restore_saved_exec_id(error_msg);
	return (XORP_ERROR);
    }

    // We can't rely on end-of-file indicators alone on Win32 to determine
    // when a child process died; we must wait for it in the event loop.
    if (!_eventloop.add_ioevent_cb(
	    _ph,
	    IOT_EXCEPTION,
	    callback(this, &RunCommandBase::win_proc_done_cb)))
	XLOG_FATAL("Cannot add child process handle to event loop.\n");
#endif // HOST_OS_WINDOWS

    // Create the stdout and stderr readers
    _stdout_file_reader = new AsyncFileReader(_eventloop,
					      XorpFd(fileno(_stdout_stream)));
    _stdout_file_reader->add_buffer(
	_stdout_buffer,
	BUF_SIZE,
	callback(this, &RunCommandBase::append_data));
    if (! _stdout_file_reader->start()) {
	XLOG_ERROR("Failed to start a stdout reader for command \"%s\"",
		   final_command.c_str());
	cleanup();
	_exec_id.restore_saved_exec_id(error_msg);
	return (XORP_ERROR);
    }

    _stderr_file_reader = new AsyncFileReader(_eventloop,
					      XorpFd(fileno(_stderr_stream)));
    _stderr_file_reader->add_buffer(
	_stderr_buffer,
	BUF_SIZE,
	callback(this, &RunCommandBase::append_data));
    if (! _stderr_file_reader->start()) {
	XLOG_ERROR("Failed to start a stderr reader for command \"%s\"",
		   final_command.c_str());
	cleanup();
	_exec_id.restore_saved_exec_id(error_msg);
	return (XORP_ERROR);
    }

    _is_running = true;

    //
    // Restore the saved execution ID
    //
    _exec_id.restore_saved_exec_id(error_msg);

    //
    // Unblock the child signals that were blocked earlier
    //
    unblock_child_signals();

    return (XORP_OK);
}

void
RunCommandBase::terminate()
{
    terminate_process(false);
}

void
RunCommandBase::terminate_with_prejudice()
{
    terminate_process(true);
}

void
RunCommandBase::terminate_process(bool with_prejudice)
{
    // Kill the process group
    if (0 != _pid) {
#ifdef HOST_OS_WINDOWS
	UNUSED(with_prejudice);
	//
	// _ph will be invalid if the process didn't start.
	// _ph will be valid if the process handle is open but the
	// process is no longer running. Calling TerminateProcess()
	// on a valid handle to a process which has terminated results
	// in an ACCESS_DENIED error.
	// Don't close the handle. pclose2() does this.
	//
	if (_ph != INVALID_HANDLE_VALUE) {
	    DWORD result; 
	    GetExitCodeProcess(_ph, &result);
	    if (result == STILL_ACTIVE) {
		DWORD result = 0;
		result = TerminateProcess(_ph, 32768);
		if (result == 0) {
		    XLOG_WARNING("TerminateProcess(): %s",
				 win_strerror(GetLastError()));
		}
	    }
	}
#else // ! HOST_OS_WINDOWS
	if (with_prejudice)
	    killpg(_pid, SIGKILL);
	else
	    killpg(_pid, SIGTERM);
#endif // ! HOST_OS_WINDOWS
    }
}

void
RunCommandBase::wait_status_changed(int wait_status)
{
    set_command_status(wait_status);

    //
    // XXX: Schedule a timer to complete the command so we can return
    // control to the caller.
    //
    // TODO: Temporary print any errors and catch any exceptions
    // (for debugging purpose).
    try {
	errno = 0;
	_done_timer.schedule_now();
    } catch(...) {
	XLOG_ERROR("Error scheduling RunCommand::_done_timer: %d", errno);
	xorp_catch_standard_exceptions();
    }
}

void
RunCommandBase::close_output()
{
    //
    // XXX: we should close stderr before stdout, because
    // close_stdout_output() invokes pclose2() that performs the
    // final cleanup.
    //
    close_stderr_output();
    close_stdout_output();
}

void
RunCommandBase::close_stdout_output()
{
    if (_stdout_file_reader != NULL) {
	delete _stdout_file_reader;
	_stdout_file_reader = NULL;
    }

    if (_stdout_stream != NULL) {
#ifdef HOST_OS_WINDOWS
	// pclose2() will close the process handle from under us.
	_eventloop.remove_ioevent_cb(_ph, IOT_EXCEPTION);
	_ph = INVALID_HANDLE_VALUE;

	//
	// XXX: in case of Windows we don't use the SIGCHLD mechanism
	// hence the process is done when the I/O is completed.
	//
	int status = pclose2(_stdout_stream, false);
	_stdout_stream = NULL;
	set_command_status(status);
	_command_is_exited = true;	// XXX: A Windows hack

#else // ! HOST_OS_WINDOWS
	pclose2(_stdout_stream, true);
	_stdout_stream = NULL;
#endif // ! HOST_OS_WINDOWS
    }
}

void
RunCommandBase::close_stderr_output()
{
    if (_stderr_file_reader != NULL) {
	delete _stderr_file_reader;
	_stderr_file_reader = NULL;
    }

    //
    // XXX: don't call pclose2(_stderr_stream), because pclose2(_stdout_stream)
    // automatically takes care of the _stderr_stream as well.
    //
    _stderr_stream = NULL;
}

void
RunCommandBase::set_command_status(int status)
{
    // Reset state
    _command_is_exited = false;
    _command_is_signal_terminated = false;
    _command_is_coredumped = false;
    _command_is_stopped = false;
    _command_exit_status = 0;
    _command_term_signal = 0;
    _command_stop_signal = 0;

    // Set the command status
#ifdef HOST_OS_WINDOWS
    _command_exit_status = status;
#else // ! HOST_OS_WINDOWS
    if (status >= 0) {
	_command_is_exited = WIFEXITED(status);
	_command_is_signal_terminated = WIFSIGNALED(status);
	_command_is_stopped = WIFSTOPPED(status);
	if (_command_is_exited)
	    _command_exit_status = WEXITSTATUS(status);
	if (_command_is_signal_terminated) {
	    _command_term_signal = WTERMSIG(status);
	    _command_is_coredumped = WCOREDUMP(status);
	}
	if (_command_is_stopped) {
	    _command_stop_signal = WSTOPSIG(status);
	}
    }
#endif // ! HOST_OS_WINDOWS

    if (_command_is_stopped)
	stopped_cb_dispatch(_command_stop_signal);
}

void
RunCommandBase::append_data(AsyncFileOperator::Event	event,
			    const uint8_t*		buffer,
			    size_t			/* buffer_bytes */,
			    size_t			offset)
{
    size_t* last_offset_ptr = NULL;
    bool is_stdout = false;

    if (buffer == _stdout_buffer) {
	is_stdout = true;
	last_offset_ptr = &_last_stdout_offset;
    } else {
	XLOG_ASSERT(buffer == _stderr_buffer);
	is_stdout = false;
	last_offset_ptr = &_last_stderr_offset;
    }

    if ((event != AsyncFileOperator::END_OF_FILE)
	&& (event != AsyncFileOperator::DATA)) {
	//
	// Something bad happened.
	// XXX: ideally we'd figure out what.
	//
	int error_code = 0;
	if (is_stdout)
	    error_code = _stdout_file_reader->error();
	else
	    error_code = _stderr_file_reader->error();
	io_done(event, error_code);
	return;
    }

    //
    // Either DATA or END_OF_FILE
    //
    XLOG_ASSERT(offset >= *last_offset_ptr);

    if (offset != *last_offset_ptr) {
	const char* p   = (const char*)buffer + *last_offset_ptr;
	size_t      len = offset - *last_offset_ptr;
	debug_msg("len = %u\n", XORP_UINT_CAST(len));
	if (!_is_error) {
	    if (is_stdout)
		stdout_cb_dispatch(string(p, len));
	    else
		stderr_cb_dispatch(string(p, len));
	} else {
	    _error_msg.append(p, p + len);
	}
	*last_offset_ptr = offset;
    }

    if (offset == BUF_SIZE) {
	// The buffer is exhausted
	*last_offset_ptr = 0;
	if (is_stdout) {
	    memset(_stdout_buffer, 0, BUF_SIZE);
	    _stdout_file_reader->add_buffer(
		_stdout_buffer,
		BUF_SIZE,
		callback(this, &RunCommandBase::append_data));
	    _stdout_file_reader->start();
	} else {
	    memset(_stderr_buffer, 0, BUF_SIZE);
	    _stderr_file_reader->add_buffer(
		_stderr_buffer,
		BUF_SIZE,
		callback(this, &RunCommandBase::append_data));
	    _stderr_file_reader->start();
	}
    }

    if (event == AsyncFileOperator::END_OF_FILE) {
	if (is_stdout) {
	    _stdout_eof_received = true;
	} else {
	    _stderr_eof_received = true;
	}
	do {
	    if (_stdout_eof_received
		&& (_stderr_eof_received || redirect_stderr_to_stdout())) {
		io_done(event, 0);
		break;
	    }
	    if ((! is_stdout) && _stderr_eof_received) {
		close_stderr_output();
		break;
	    }
	    break;
	} while (false);
	return;
    }
}

void
RunCommandBase::io_done(AsyncFileOperator::Event event, int error_code)
{
    if (event != AsyncFileOperator::END_OF_FILE) {
	string prefix, suffix;

	_is_error = true;
	if (_error_msg.size()) {
	    prefix = "[";
	    suffix = "]";
	}
	_error_msg += prefix;
	_error_msg += c_format("Command \"%s\" terminated because of "
			       "unexpected event (event = 0x%x error = %d).",
			       _real_command_name.c_str(), event, error_code);
	_error_msg += suffix;
	terminate_with_prejudice();
    }

    close_output();

    done(_done_timer);
}

void
RunCommandBase::done(XorpTimer& done_timer)
{
    string prefix, suffix, reason;

    done_timer.unschedule();

    if (_stdout_stream != NULL)
	return;		// XXX: I/O is not done yet

    if (! (_command_is_exited || _command_is_signal_terminated))
	return;		// XXX: the command is not done yet

    // Remove the entry from the pid map
    pid2command.erase(_pid);
    _pid = 0;
    _done_timer.unschedule();
    _is_running = false;

    if (_error_msg.size()) {
	prefix = "[";
	suffix = "]";
    }
    _error_msg += prefix;

    if (_command_is_exited && (_command_exit_status != 0)) {
	_is_error = true;
	if (! reason.empty())
	    reason += "; ";
	reason += c_format("exited with exit status %d",
			   _command_exit_status);
    }
    if (_command_is_signal_terminated) {
	_is_error = true;
	if (! reason.empty())
	    reason += "; ";
	reason += c_format("terminated with signal %d",
			   _command_term_signal);
    }
    if (_command_is_coredumped) {
	_is_error = true;
	if (! reason.empty())
	    reason += "; ";
	reason += c_format("aborted with a core dump");
    }
    if (! reason.empty()) {
	_error_msg += c_format("Command \"%s\": %s.",
			       _real_command_name.c_str(),
			       reason.c_str());
    }

    _error_msg += suffix;

    done_cb_dispatch(!_is_error, _error_msg);

    //
    // XXX: the callback will delete us. Don't do anything more in this method.
    //
    // delete this;
}

void
RunCommandBase::set_exec_id(const ExecId& v)
{
    _exec_id = v;
}

RunCommandBase::ExecId::ExecId()
    : _uid(0),
      _gid(0),
      _is_uid_set(false),
      _is_gid_set(false),
      _saved_uid(0),
      _saved_gid(0),
      _is_exec_id_saved(false)
{

}

RunCommandBase::ExecId::ExecId(uid_t uid)
    : _uid(uid),
      _gid(0),
      _is_uid_set(true),
      _is_gid_set(false),
      _saved_uid(0),
      _saved_gid(0),
      _is_exec_id_saved(false)
{

}

RunCommandBase::ExecId::ExecId(uid_t uid, gid_t gid)
    : _uid(uid),
      _gid(gid),
      _is_uid_set(true),
      _is_gid_set(true),
      _saved_uid(0),
      _saved_gid(0),
      _is_exec_id_saved(false)
{

}

void
RunCommandBase::ExecId::save_current_exec_id()
{
#ifndef HOST_OS_WINDOWS
    _saved_uid = getuid();
    _saved_gid = getgid();
#endif
    _is_exec_id_saved = true;
}

int
RunCommandBase::ExecId::restore_saved_exec_id(string& error_msg) const
{
#ifdef HOST_OS_WINDOWS
    UNUSED(error_msg);
#else // ! HOST_OS_WINDOWS
    if (! _is_exec_id_saved)
	return (XORP_OK);	// Nothing to do, because nothing was saved

    if (seteuid(saved_uid()) != 0) {
	error_msg = c_format("Cannot restore saved user ID to %u: %s",
			     XORP_UINT_CAST(saved_uid()), strerror(errno));
	return (XORP_ERROR);
    }

    if (setegid(saved_gid()) != 0) {
	error_msg = c_format("Cannot restore saved group ID to %u: %s",
			     XORP_UINT_CAST(saved_gid()), strerror(errno));
	return (XORP_ERROR);
    }
#endif // ! HOST_OS_WINDOWS

    return (XORP_OK);
}

int
RunCommandBase::ExecId::set_effective_exec_id(string& error_msg)
{
#ifdef HOST_OS_WINDOWS
    UNUSED(error_msg);
#else // ! HOST_OS_WINDOWS
    if (! is_set())
	return (XORP_OK);

    //
    // Set the effective group ID
    //
    if (is_gid_set() && (gid() != saved_gid())) {
	if (setegid(gid()) != 0) {
	    error_msg = c_format("Cannot set the effective group ID to %u: %s",
				 XORP_UINT_CAST(gid()), strerror(errno));
	    return (XORP_ERROR);
	}
    }

    //
    // Set the effective user ID
    //
    if (is_uid_set() && (uid() != saved_uid())) {
	if (seteuid(uid()) != 0) {
	    error_msg = c_format("Cannot set effective user ID to %u: %s",
				 XORP_UINT_CAST(uid()), strerror(errno));
	    return (XORP_ERROR);
	}
    }
#endif // ! HOST_OS_WINDOWS

    return (XORP_OK);
}

bool
RunCommandBase::ExecId::is_set() const
{
    return (is_uid_set() || is_gid_set());
}

void
RunCommandBase::ExecId::reset()
{
    _uid = 0;
    _gid = 0;
    _is_uid_set = false;
    _is_gid_set = false;
    _saved_uid = 0;
    _saved_gid = 0;
    _is_exec_id_saved = false;
}

#ifdef HOST_OS_WINDOWS
//
// Hack to get asynchronous notification of Win32 process death.
// The process handle will be SIGNALLED when the process terminates.
// Because Win32 pipes must be polled, we must make sure that RunCommand
// does not tear down the pipes until everything is read, otherwise the
// process death event will be dispatched before the pipe I/O events.
// We schedule a timer to make sure output is read from the pipes
// before they close, as Win32 pipes lose data when closed; the parent
// must hold handles for both ends of the pipe, unlike UNIX.
//
void
RunCommandBase::win_proc_done_cb(XorpFd fd, IoEventType type)
{
    XLOG_ASSERT(type == IOT_EXCEPTION);
    XLOG_ASSERT(static_cast<HANDLE>(fd) == _ph);
    _eventloop.remove_ioevent_cb(_ph, IOT_EXCEPTION);
    _reaper_timer = _eventloop.new_oneoff_after_ms(
	WIN32_PROC_TIMEOUT_MS,
	callback(this, &RunCommandBase::win_proc_reaper_cb));
    XLOG_ASSERT(_reaper_timer.scheduled());
}

void
RunCommandBase::win_proc_reaper_cb()
{
    io_done(AsyncFileOperator::END_OF_FILE, 0);
}
#endif // HOST_OS_WINDOWS


syntax highlighted by Code2HTML, v. 0.9.1