/**
 * @file sys_start_process.c
 * @author Ambroz Bizjak <ambrop7@gmail.com>
 * 
 * @section LICENSE
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the author nor the
 *    names of its contributors may be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * @section DESCRIPTION
 * 
 * Synopsis:
 *   sys.start_process(list command, string mode [, map options])
 * 
 * Options:
 *   "keep_stdout":"true" - Start the program with the same stdout as the NCD process.
 *     Must not be present if the process is being opened for reading.
 *   "keep_stderr":true" - Start the program with the same stderr as the NCD process.
 *   "do_setsid":"true" - Call setsid() in the child before exec. This is needed to
 *     start the 'agetty' program.
 *   "username":username_string - Start the process under the permissions of the
 *     specified user. 
 *   "term_on_deinit":"false" - do not send SIGTERM to the process when this statement
 *     is requested to terminate
 *   "deinit_kill_time":milliseconds - how long to wait for the process to terminate
 *     after this statement is requested to terminate until we send SIGKILL. If this option
 *     is not present or is "never", SIGKILL will not be sent. If this option is empty, the
 *     process will be sent SIGKILL immediately when the statement is requested to terminate.
 * 
 * Variables:
 *   is_error - "true" if there was an error starting the process, "false" if the process
 *              has been started successfully
 * 
 * Synopsis:
 *   sys.start_process::wait()
 * 
 * Variables:
 *   exit_status - the exit code if the process terminated normally, -1 if it terminated
 *                 with a signal
 * 
 * Synopsis:
 *   sys.start_process::terminate()
 *   sys.start_process::kill()
 * 
 * Synopsis:
 *   sys.start_process::read_pipe()
 * 
 * Description:
 *   Creates a read interface to the process's standard output. Data is read using the
 *   read() method on this object. Read errors are reported implicitly by this statement
 *   going down and the 'is_error' variable changing to "true".
 *   When read_pipe() is initialized for a process, it takes ownership of the read pipe
 *   to the process. When read_pipe() is requested to terminate, it will close the pipe.
 *   Attempting to initialize read_pipe() on a process which was not started with 'r'
 *   in the mode argument, or where another read_pipe() object has already taken ownership
 *   of the read pipe, will result in throwing an error to the interpreter.
 * 
 * Variables:
 *   string is_error - "true" if there was a read error, "false" if not
 * 
 * Synopsis:
 *   sys.start_process::read_pipe::read()
 * 
 * Description:
 *   Reads some data. If a read error occurs, it is reported implicitly via the
 *   read_pipe() object going down. If end of file is reached, this and any future read()
 *   operations will indicate that via the 'not_eof' variable. It is guaranteed that after
 *   EOF is reached, the read_pipe() object will not go down to report any errors.
 *   WARNING: if a read() is requested to terminate before it has completed, the
 *            read_pipe() will become unusable and any read() invocation after that will
 *            throw an error to the interpreter.
 * 
 * Variables:
 *   string (empty) - data that was read, or an empty string on EOF
 *   string not_eof - "true" is EOF was not reached, "false" if it was
 * 
 * Synopsis:
 *   sys.start_process::write_pipe()
 * 
 * Description:
 *   Creates a write interface to the process's standard input. Data is written using the
 *   write() method on this object. Write errors are reported implicitly by this statement
 *   going down and the ''is_error variable changing to "true".
 *   When write_pipe() is initialized for a process, it takes ownership of the write pipe
 *   to the process. When write_pipe() is requested to terminate, it will close the pipe
 *   (unless the close() has been used).
 *   Attempting to initialize write_pipe() on a process which was not started with 'w'
 *   in the mode argument, or where another write_pipe() object has already taken ownership
 *   of the write pope, will result in throwing an error to the interpreter.
 * 
 * Variables:
 *   string is_error - "true" if there was a write error, "false" if not
 * 
 * Synopsis:
 *   sys.start_process::write_pipe::write(string data)
 * 
 * Description:
 *   Writes the given data. If a write error occurs, it is reported implicitly via the
 *   write_pipe() object going down.
 *   WARNING: if a write() is requested to terminate before it has completed, the
 *            write_pipe() will become unusable and any write() or close() invocation after
 *            that will throw an error to the interpreter.
 * 
 * Synopsis:
 *   sys.start_process::write_pipe::close(string data)
 * 
 * Description:
 *   Closes the write pipe. This will make whatever is reading the other end of the pipe
 *   encounter EOF after it has read any pending data. It is guaranteed that after the
 *   pipe is closed, the write_pipe() object will not go down to report any errors.
 *   After close() is performed, any further write() or close() calls are disallowed and
 *   will throw errors to the interpreter.
 */

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <inttypes.h>
#include <limits.h>
#include <unistd.h>

#include <misc/offset.h>
#include <structure/LinkedList0.h>
#include <system/BProcess.h>
#include <system/BConnection.h>
#include <ncd/NCDModule.h>
#include <ncd/static_strings.h>
#include <ncd/extra/NCDBuf.h>
#include <ncd/extra/value_utils.h>
#include <ncd/extra/build_cmdline.h>
#include <ncd/extra/NCDBProcessOpts.h>

#include <generated/blog_channel_ncd_sys_start_process.h>

#define ModuleLog(i, ...) NCDModuleInst_Backend_Log((i), BLOG_CURRENT_CHANNEL, __VA_ARGS__)

#define READ_BUF_SIZE 8192

#define PROCESS_STATE_ERROR 1
#define PROCESS_STATE_RUNNING 2
#define PROCESS_STATE_TERMINATED 3
#define PROCESS_STATE_DYING 4

#define READER_STATE_RUNNING 1
#define READER_STATE_EOF 2
#define READER_STATE_ERROR 3
#define READER_STATE_ABORTED 4

#define WRITER_STATE_RUNNING 1
#define WRITER_STATE_CLOSED 2
#define WRITER_STATE_ERROR 3
#define WRITER_STATE_ABORTED 4

struct process_instance {
    NCDModuleInst *i;
    BProcess process;
    BSmallTimer kill_timer;
    LinkedList0 waits_list;
    btime_t deinit_kill_time;
    int term_on_deinit;
    int read_fd;
    int write_fd;
    int exit_status;
    int state;
};

struct wait_instance {
    NCDModuleInst *i;
    struct process_instance *pinst;
    LinkedList0Node waits_list_node;
    int exit_status;
};

struct read_pipe_instance {
    NCDModuleInst *i;
    int state;
    int read_fd;
    BConnection connection;
    NCDBufStore store;
    struct read_instance *read_inst;
};

struct read_instance {
    NCDModuleInst *i;
    struct read_pipe_instance *read_pipe_inst;
    NCDBuf *buf;
    size_t read_size;
};

struct write_pipe_instance {
    NCDModuleInst *i;
    int state;
    int write_fd;
    BConnection connection;
    struct write_instance *write_inst;
};

struct write_instance {
    NCDModuleInst *i;
    struct write_pipe_instance *write_pipe_inst;
    b_cstring cstr;
    size_t pos;
};

static int parse_mode (NCDModuleInst *i, NCDValRef mode_arg, int *out_read, int *out_write)
{
    if (!NCDVal_IsString(mode_arg)) {
        ModuleLog(i, BLOG_ERROR, "mode argument must be a string");
        return 0;
    }
    
    *out_read = 0;
    *out_write = 0;
    
    b_cstring cstr = NCDVal_StringCstring(mode_arg);
    
    B_CSTRING_LOOP_CHARS(cstr, char_pos, ch, {
        if (ch == 'r') {
            *out_read = 1;
        }
        else if (ch == 'w') {
            *out_write = 1;
        }
        else {
            ModuleLog(i, BLOG_ERROR, "invalid character in mode argument");
            return 0;
        }
    })
    
    return 1;
}

static void process_free (struct process_instance *o)
{
    // close write fd
    if (o->write_fd != -1) {
        if (close(o->write_fd) < 0) {
            ModuleLog(o->i, BLOG_ERROR, "close failed");
        }
    }
    
    // close read fd
    if (o->read_fd != -1) {
        if (close(o->read_fd) < 0) {
            ModuleLog(o->i, BLOG_ERROR, "close failed");
        }
    }
    
    NCDModuleInst_Backend_Dead(o->i);
}

static void process_handler (void *vo, int normally, uint8_t normally_exit_status)
{
    struct process_instance *o = vo;
    ASSERT(o->state == PROCESS_STATE_RUNNING || o->state == PROCESS_STATE_DYING)
    
    ModuleLog(o->i, BLOG_INFO, "process terminated");
    
    // free kill timer
    BReactor_RemoveSmallTimer(o->i->params->iparams->reactor, &o->kill_timer);
    
    // free process
    BProcess_Free(&o->process);
    
    // remember exit code
    o->exit_status = (!normally ? -1 : normally_exit_status);
    
    // finish waits
    LinkedList0Node *ln;
    while ((ln = LinkedList0_GetFirst(&o->waits_list))) {
        struct wait_instance *winst = UPPER_OBJECT(ln, struct wait_instance, waits_list_node);
        ASSERT(winst->pinst == o)
        LinkedList0_Remove(&o->waits_list, &winst->waits_list_node);
        winst->pinst = NULL;
        winst->exit_status = o->exit_status;
        NCDModuleInst_Backend_Up(winst->i);
    }
    
    // if we have been requested to die, then die now
    if (o->state == PROCESS_STATE_DYING) {
        process_free(o);
        return;
    }
    
    // set state
    o->state = PROCESS_STATE_TERMINATED;
}

static void process_kill_timer_handler (BSmallTimer *kill_timer)
{
    struct process_instance *o = UPPER_OBJECT(kill_timer, struct process_instance, kill_timer);
    ASSERT(o->state == PROCESS_STATE_DYING)
    
    ModuleLog(o->i, BLOG_INFO, "killing process after timeout");
    BProcess_Kill(&o->process);
}

static int opts_func_unknown (void *user, NCDValRef key, NCDValRef val)
{
    struct process_instance *o = user;
    
    if (NCDVal_IsString(key) && NCDVal_StringEquals(key, "term_on_deinit")) {
        o->term_on_deinit = ncd_read_boolean(val);
        return 1;
    }
    
    if (NCDVal_IsString(key) && NCDVal_StringEquals(key, "deinit_kill_time")) {
        if (NCDVal_StringEquals(val, "never")) {
            o->deinit_kill_time = -2;
        }
        else if (NCDVal_StringEqualsId(val, NCD_STRING_EMPTY, o->i->params->iparams->string_index)) {
            o->deinit_kill_time = -1;
        }
        else if (!ncd_read_time(val, &o->deinit_kill_time)) {
            ModuleLog(o->i, BLOG_ERROR, "wrong value for deinit_kill_time option");
            return 0;
        }
        return 1;
    }
    
    return 0;
}

static void process_func_new (void *vo, NCDModuleInst *i, const struct NCDModuleInst_new_params *params)
{
    struct process_instance *o = vo;
    o->i = i;
    NCDModuleInst_Backend_PassMemToMethods(i);
    
    // check arguments
    NCDValRef command_arg;
    NCDValRef mode_arg;
    NCDValRef options_arg = NCDVal_NewInvalid();
    if (!NCDVal_ListRead(params->args, 2, &command_arg, &mode_arg) &&
        !NCDVal_ListRead(params->args, 3, &command_arg, &mode_arg, &options_arg)
    ) {
        ModuleLog(i, BLOG_ERROR, "wrong arity");
        goto fail0;
    }
    
    // parse mode
    int is_read;
    int is_write;
    if (!parse_mode(i, mode_arg, &is_read, &is_write)) {
        goto fail0;
    }
    
    // parse options
    NCDBProcessOpts opts;
    int keep_stdout;
    int keep_stderr;
    o->deinit_kill_time = -2;
    o->term_on_deinit = 1;
    if (!NCDBProcessOpts_Init2(&opts, options_arg, opts_func_unknown, o, i, BLOG_CURRENT_CHANNEL, &keep_stdout, &keep_stderr)) {
        goto fail0;
    }
    
    // keep-stdout option and read mode are not compatible
    if (keep_stdout && is_read) {
        ModuleLog(i, BLOG_ERROR, "keep-stdout and read mode are not compatible");
        goto fail1;
    }
    
    // prepare for creating pipes
    int fds[4];
    int fds_map[3];
    int start_num_fds = opts.nfds;
    int num_fds = start_num_fds;
    memcpy(fds, opts.fds, num_fds * sizeof(int));
    memcpy(fds_map, opts.fds_map, num_fds * sizeof(int));
    int read_fd = -1;
    int write_fd = -1;
    
    // create read pipe
    if (is_read) {
        int pipefd[2];
        if (pipe(pipefd) < 0) {
            ModuleLog(i, BLOG_ERROR, "pipe failed");
            goto error1;
        }
        read_fd = pipefd[0];
        fds[num_fds] = pipefd[1];
        fds_map[num_fds++] = STDOUT_FILENO;
    }
    
    // create write pipe
    if (is_write) {
        int pipefd[2];
        if (pipe(pipefd) < 0) {
            ModuleLog(i, BLOG_ERROR, "pipe failed");
            goto error1;
        }
        write_fd = pipefd[1];
        fds[num_fds] = pipefd[0];
        fds_map[num_fds++] = STDIN_FILENO;
    }
    
    // terminate fds array
    fds[num_fds] = -1;
    
    // build process parameters struct
    struct BProcess_params p_params = {};
    p_params.fds = fds;
    p_params.fds_map = fds_map;
    p_params.do_setsid = opts.do_setsid;
    p_params.username = opts.username;
    
    // build command line
    char *exec;
    CmdLine cl;
    if (!ncd_build_cmdline(i, BLOG_CURRENT_CHANNEL, command_arg, &exec, &cl)) {
        goto error1;
    }
    
    // start process
    int res = BProcess_Init2(&o->process, i->params->iparams->manager, process_handler, o, exec, CmdLine_Get(&cl), p_params);
    CmdLine_Free(&cl);
    free(exec);
    if (!res) {
        ModuleLog(i, BLOG_ERROR, "BProcess_Init failed");
        goto error1;
    }
    
    // init kill timer
    BSmallTimer_Init(&o->kill_timer, process_kill_timer_handler);
    
    // close child fds
    while (num_fds-- > start_num_fds) {
        if (close(fds[num_fds]) < 0) {
            ModuleLog(i, BLOG_ERROR, "close failed");
        }
    }
    
    // free opts
    NCDBProcessOpts_Free(&opts);
    
    // init waits list
    LinkedList0_Init(&o->waits_list);
    
    // remember our fds
    o->read_fd = read_fd;
    o->write_fd = write_fd;
    
    // set state
    o->state = PROCESS_STATE_RUNNING;
    
    // go up
    NCDModuleInst_Backend_Up(i);
    return;
    
fail1:
    NCDBProcessOpts_Free(&opts);
fail0:
    NCDModuleInst_Backend_DeadError(i);
    return;
    
error1:
    if (write_fd != -1) {
        if (close(write_fd) < 0) {
            ModuleLog(i, BLOG_ERROR, "close failed");
        }
    }
    if (read_fd != -1) {
        if (close(read_fd) < 0) {
            ModuleLog(i, BLOG_ERROR, "close failed");
        }
    }
    while (num_fds-- > start_num_fds) {
        if (close(fds[num_fds]) < 0) {
            ModuleLog(i, BLOG_ERROR, "close failed");
        }
    }
    NCDBProcessOpts_Free(&opts);
    
    o->read_fd = -1;
    o->write_fd = -1;
    o->state = PROCESS_STATE_ERROR;
    NCDModuleInst_Backend_Up(i);
}

static void process_func_die (void *vo)
{
    struct process_instance *o = vo;
    ASSERT(o->state != PROCESS_STATE_DYING)
    
    // if process is not running, die immediately
    if (o->state != PROCESS_STATE_RUNNING) {
        process_free(o);
        return;
    }
    
    if (o->term_on_deinit) {
        ModuleLog(o->i, BLOG_INFO, "terminating process");
        
        // send termination signal
        BProcess_Terminate(&o->process);
    } else {
        ModuleLog(o->i, BLOG_INFO, "not terminating process as requested");
    }
    
    if (o->deinit_kill_time == -1) {
        // user wants SIGKILL immediately
        ModuleLog(o->i, BLOG_INFO, "killing process immediately");
        BProcess_Kill(&o->process);
    } else if (o->deinit_kill_time >= 0) {
        // user wants SIGKILL after some time
        BReactor_SetSmallTimer(o->i->params->iparams->reactor, &o->kill_timer, BTIMER_SET_RELATIVE, o->deinit_kill_time);
    }
    
    // set state
    o->state = PROCESS_STATE_DYING;
}

static int process_func_getvar (void *vo, NCD_string_id_t name, NCDValMem *mem, NCDValRef *out)
{
    struct process_instance *o = vo;
    
    if (name == NCD_STRING_IS_ERROR) {
        int is_error = (o->state == PROCESS_STATE_ERROR);
        *out = ncd_make_boolean(mem, is_error, o->i->params->iparams->string_index);
        return 1;
    }
    
    return 0;
}

static void wait_func_new (void *vo, NCDModuleInst *i, const struct NCDModuleInst_new_params *params)
{
    struct wait_instance *o = vo;
    o->i = i;
    
    if (!NCDVal_ListRead(params->args, 0)) {
        ModuleLog(i, BLOG_ERROR, "wrong arity");
        goto fail0;
    }
    
    struct process_instance *pinst = params->method_user;
    
    if (pinst->state == PROCESS_STATE_ERROR) {
        ModuleLog(i, BLOG_ERROR, "wait() is disallowed after the process has failed to start");
        goto fail0;
    }
    
    if (pinst->state == PROCESS_STATE_TERMINATED) {
        // not waiting, set no pinst
        o->pinst = NULL;
        
        // remember exit code
        o->exit_status = pinst->exit_status;
        
        // go up
        NCDModuleInst_Backend_Up(i);
    } else {
        // waitint, set pinst
        o->pinst = pinst;
        
        // insert to waits list
        LinkedList0_Prepend(&pinst->waits_list, &o->waits_list_node);
    }
    
    return;
    
fail0:
    NCDModuleInst_Backend_DeadError(i);
}

static void wait_func_die (void *vo)
{
    struct wait_instance *o = vo;
    
    // remove from waits list
    if (o->pinst) {
        LinkedList0_Remove(&o->pinst->waits_list, &o->waits_list_node);
    }
    
    NCDModuleInst_Backend_Dead(o->i);
}

static int wait_func_getvar (void *vo, NCD_string_id_t name, NCDValMem *mem, NCDValRef *out)
{
    struct wait_instance *o = vo;
    ASSERT(!o->pinst)
    
    if (name == NCD_STRING_EXIT_STATUS) {
        if (o->exit_status == -1) {
            *out = NCDVal_NewString(mem, "-1");
        } else {
            *out = ncd_make_uintmax(mem, o->exit_status);
        }
        return 1;
    }
    
    return 0;
}

static void terminate_kill_new_common (void *vo, NCDModuleInst *i, const struct NCDModuleInst_new_params *params, int is_kill)
{
    if (!NCDVal_ListRead(params->args, 0)) {
        ModuleLog(i, BLOG_ERROR, "wrong arity");
        goto fail0;
    }
    
    struct process_instance *pinst = params->method_user;
    
    if (pinst->state == PROCESS_STATE_ERROR) {
        ModuleLog(i, BLOG_ERROR, "terminate()/kill() is disallowed after the process has failed to start");
        goto fail0;
    }
    
    if (pinst->state != PROCESS_STATE_TERMINATED) {
        if (is_kill) {
            BProcess_Kill(&pinst->process);
        } else {
            BProcess_Terminate(&pinst->process);
        }
    }
    
    NCDModuleInst_Backend_Up(i);
    return;
    
fail0:
    NCDModuleInst_Backend_DeadError(i);
}

static void terminate_func_new (void *vo, NCDModuleInst *i, const struct NCDModuleInst_new_params *params)
{
    terminate_kill_new_common(vo, i, params, 0);
}

static void kill_func_new (void *vo, NCDModuleInst *i, const struct NCDModuleInst_new_params *params)
{
    terminate_kill_new_common(vo, i, params, 1);
}

static void read_pipe_free_connection (struct read_pipe_instance *o)
{
    // disconnect read instance
    if (o->read_inst) {
        ASSERT(o->read_inst->read_pipe_inst == o)
        o->read_inst->read_pipe_inst = NULL;
    }
    
    // free store
    NCDBufStore_Free(&o->store);
    
    // free connection read interface
    BConnection_RecvAsync_Free(&o->connection);
    
    // free connection
    BConnection_Free(&o->connection);
    
    // close fd
    if (close(o->read_fd) < 0) {
        ModuleLog(o->i, BLOG_ERROR, "close failed");
    }
}

static void read_pipe_abort (struct read_pipe_instance *o)
{
    ASSERT(o->state == READER_STATE_RUNNING)
    
    // release connection resources
    read_pipe_free_connection(o);
    
    // set state
    o->state = READER_STATE_ABORTED;
}

static void read_pipe_connection_handler (void *vo, int event)
{
    struct read_pipe_instance *o = vo;
    ASSERT(o->state == READER_STATE_RUNNING)
    
    if (event == BCONNECTION_EVENT_RECVCLOSED) {
        // if we have read operation, make it finish with eof
        if (o->read_inst) {
            ASSERT(o->read_inst->read_pipe_inst == o)
            ASSERT(o->read_inst->buf)
            o->read_inst->read_pipe_inst = NULL;
            o->read_inst->read_size = 0;
            NCDModuleInst_Backend_Up(o->read_inst->i);
            o->read_inst = NULL;
        }
        
        // free connection resources
        read_pipe_free_connection(o);
        
        // set state closed
        o->state = READER_STATE_EOF;
        return;
    }
    
    ModuleLog(o->i, BLOG_ERROR, "read pipe error");
    
    // free connection resources
    read_pipe_free_connection(o);
    
    // set state error
    o->state = READER_STATE_ERROR;
    
    // backtrack
    NCDModuleInst_Backend_DownUp(o->i);
}

static void read_pipe_recv_handler_done (void *vo, int data_len)
{
    struct read_pipe_instance *o = vo;
    ASSERT(o->state == READER_STATE_RUNNING)
    ASSERT(o->read_inst)
    ASSERT(o->read_inst->read_pipe_inst == o)
    ASSERT(o->read_inst->buf)
    ASSERT(data_len > 0)
    ASSERT(data_len <= NCDBufStore_BufSize(&o->store))
    
    // finish read operation
    o->read_inst->read_pipe_inst = NULL;
    o->read_inst->read_size = data_len;
    NCDModuleInst_Backend_Up(o->read_inst->i);
    o->read_inst = NULL;
}

static void read_pipe_func_new (void *vo, NCDModuleInst *i, const struct NCDModuleInst_new_params *params)
{
    struct read_pipe_instance *o = vo;
    o->i = i;
    NCDModuleInst_Backend_PassMemToMethods(i);
    
    if (!NCDVal_ListRead(params->args, 0)) {
        ModuleLog(i, BLOG_ERROR, "wrong arity");
        goto fail0;
    }
    
    struct process_instance *pinst = params->method_user;
    
    if (pinst->read_fd == -1) {
        ModuleLog(i, BLOG_ERROR, "process did not start successfully, was not opened for reading or a read_pipe was already created");
        goto fail0;
    }
    
    // init connection
    if (!BConnection_Init(&o->connection, BConnection_source_pipe(pinst->read_fd), i->params->iparams->reactor, o, read_pipe_connection_handler)) {
        ModuleLog(i, BLOG_ERROR, "BConnection_Init failed");
        goto fail0;
    }
    
    // init connection read interface
    BConnection_RecvAsync_Init(&o->connection);
    
    // set recv done callback
    StreamRecvInterface_Receiver_Init(BConnection_RecvAsync_GetIf(&o->connection), read_pipe_recv_handler_done, o);
    
    // init store
    NCDBufStore_Init(&o->store, READ_BUF_SIZE);
    
    // set variables
    o->state = READER_STATE_RUNNING;
    o->read_fd = pinst->read_fd;
    o->read_inst = NULL;
    
    // steal read fd from process instance
    pinst->read_fd = -1;
    
    // go up
    NCDModuleInst_Backend_Up(i);
    return;
    
fail0:
    NCDModuleInst_Backend_DeadError(i);
}

static void read_pipe_func_die (void *vo)
{
    struct read_pipe_instance *o = vo;
    
    // free connection resources
    if (o->state == READER_STATE_RUNNING) {
        read_pipe_free_connection(o);
    }
    
    NCDModuleInst_Backend_Dead(o->i);
}

static int read_pipe_func_getvar (void *vo, NCD_string_id_t name, NCDValMem *mem, NCDValRef *out)
{
    struct read_pipe_instance *o = vo;
    
    if (name == NCD_STRING_IS_ERROR) {
        int is_error = (o->state == READER_STATE_ERROR);
        *out = ncd_make_boolean(mem, is_error, o->i->params->iparams->string_index);
        return 1;
    }
    
    return 0;
}

static void read_func_new (void *vo, NCDModuleInst *i, const struct NCDModuleInst_new_params *params)
{
    struct read_instance *o = vo;
    o->i = i;
    
    if (!NCDVal_ListRead(params->args, 0)) {
        ModuleLog(i, BLOG_ERROR, "wrong arity");
        goto fail0;
    }
    
    struct read_pipe_instance *read_pipe_inst = params->method_user;
    
    // check if a read error has already occured
    if (read_pipe_inst->state == READER_STATE_ERROR) {
        ModuleLog(i, BLOG_ERROR, "read() is disallowed after a read error has occured");
        goto fail0;
    }
    
    // check if the read_pipe has been aborted
    if (read_pipe_inst->state == READER_STATE_ABORTED) {
        ModuleLog(i, BLOG_ERROR, "read() is disallowed after a read() has been aborted");
        goto fail0;
    }
    
    // if EOF has already been encountered, complete the read immediately
    if (read_pipe_inst->state == READER_STATE_EOF) {
        o->buf = NULL;
        o->read_pipe_inst = NULL;
        o->read_size = 0;
        NCDModuleInst_Backend_Up(i);
        return;
    }
    
    ASSERT(read_pipe_inst->state == READER_STATE_RUNNING)
    
    // check if there's already a read in progress
    if (read_pipe_inst->read_inst) {
        ModuleLog(i, BLOG_ERROR, "read() is disallowed while another read() is in progress");
        goto fail0;
    }
    
    // get buffer
    o->buf = NCDBufStore_GetBuf(&read_pipe_inst->store);
    if (!o->buf) {
        ModuleLog(i, BLOG_ERROR, "NCDBufStore_GetBuf failed");
        goto fail0;
    }
    
    // set read_pipe
    o->read_pipe_inst = read_pipe_inst;
    
    // register read in read_pipe
    read_pipe_inst->read_inst = o;
    
    // receive
    size_t buf_size = NCDBufStore_BufSize(&read_pipe_inst->store);
    int to_read = (buf_size > INT_MAX ? INT_MAX : buf_size);
    StreamRecvInterface_Receiver_Recv(BConnection_RecvAsync_GetIf(&read_pipe_inst->connection), (uint8_t *)NCDBuf_Data(o->buf), to_read);
    return;
    
fail0:
    NCDModuleInst_Backend_DeadError(i);
}

static void read_func_die (void *vo)
{
    struct read_instance *o = vo;
    
    // if we're receiving, abort read_pipe
    if (o->read_pipe_inst) {
        ASSERT(o->read_pipe_inst->state == READER_STATE_RUNNING)
        ASSERT(o->read_pipe_inst->read_inst == o)
        ASSERT(o->buf)
        read_pipe_abort(o->read_pipe_inst);
    }
    
    // release buffer
    if (o->buf) {
        BRefTarget_Deref(NCDBuf_RefTarget(o->buf));
    }
    
    NCDModuleInst_Backend_Dead(o->i);
}

static int read_func_getvar (void *vo, NCD_string_id_t name, NCDValMem *mem, NCDValRef *out)
{
    struct read_instance *o = vo;
    ASSERT(!o->read_pipe_inst)
    ASSERT(!(o->read_size > 0) || o->buf)
    
    if (name == NCD_STRING_EMPTY) {
        if (o->read_size > 0) {
            *out = NCDVal_NewExternalString(mem, NCDBuf_Data(o->buf), o->read_size, NCDBuf_RefTarget(o->buf));
        } else {
            *out = NCDVal_NewIdString(mem, NCD_STRING_EMPTY, o->i->params->iparams->string_index);
        }
        return 1;
    }
    
    if (name == NCD_STRING_NOT_EOF) {
        int not_eof = (o->read_size > 0);
        *out = ncd_make_boolean(mem, not_eof, o->i->params->iparams->string_index);
        return 1;
    }
    
    return 0;
}

static void write_pipe_free_connection (struct write_pipe_instance *o)
{
    // disconnect write instance
    if (o->write_inst) {
        ASSERT(o->write_inst->write_pipe_inst == o)
        o->write_inst->write_pipe_inst = NULL;
    }
    
    // free connection send interface
    BConnection_SendAsync_Free(&o->connection);
    
    // free connection
    BConnection_Free(&o->connection);
    
    // close fd
    if (close(o->write_fd) < 0) {
        ModuleLog(o->i, BLOG_ERROR, "close failed");
    }
}

static void write_pipe_abort (struct write_pipe_instance *o)
{
    ASSERT(o->state == WRITER_STATE_RUNNING)
    
    // release connection resources
    write_pipe_free_connection(o);
    
    // set state
    o->state = WRITER_STATE_ABORTED;
}

static void write_pipe_close (struct write_pipe_instance *o)
{
    ASSERT(o->state == WRITER_STATE_RUNNING)
    
    // release connection resources
    write_pipe_free_connection(o);
    
    // set state
    o->state = WRITER_STATE_CLOSED;
}

static void write_pipe_connection_handler (void *vo, int event)
{
    struct write_pipe_instance *o = vo;
    ASSERT(o->state == WRITER_STATE_RUNNING)
    
    ModuleLog(o->i, BLOG_ERROR, "write pipe error");
    
    // free connection resources
    write_pipe_free_connection(o);
    
    // set state error
    o->state = WRITER_STATE_ERROR;
    
    // backtrack
    NCDModuleInst_Backend_DownUp(o->i);
}

static void write_pipe_send_handler_done (void *vo, int data_len)
{
    struct write_pipe_instance *o = vo;
    ASSERT(o->state == WRITER_STATE_RUNNING)
    ASSERT(o->write_inst)
    ASSERT(o->write_inst->write_pipe_inst == o)
    ASSERT(data_len > 0)
    ASSERT(data_len <= o->write_inst->cstr.length - o->write_inst->pos)
    
    struct write_instance *wr = o->write_inst;
    
    // update write progress
    wr->pos += data_len;
    
    // if there is more data, start another write operation
    if (wr->pos < wr->cstr.length) {
        size_t chunk_length;
        const char *chunk_data = b_cstring_get(wr->cstr, wr->pos, wr->cstr.length - wr->pos, &chunk_length);
        size_t to_send = (chunk_length > INT_MAX ? INT_MAX : chunk_length);
        StreamPassInterface_Sender_Send(BConnection_SendAsync_GetIf(&o->connection), (uint8_t *)chunk_data, to_send);
        return;
    }
    
    // finish write operation
    wr->write_pipe_inst = NULL;
    NCDModuleInst_Backend_Up(wr->i);
    o->write_inst = NULL;
}

static void write_pipe_func_new (void *vo, NCDModuleInst *i, const struct NCDModuleInst_new_params *params)
{
    struct write_pipe_instance *o = vo;
    o->i = i;
    NCDModuleInst_Backend_PassMemToMethods(i);
    
    if (!NCDVal_ListRead(params->args, 0)) {
        ModuleLog(i, BLOG_ERROR, "wrong arity");
        goto fail0;
    }
    
    struct process_instance *pinst = params->method_user;
    
    if (pinst->write_fd == -1) {
        ModuleLog(i, BLOG_ERROR, "process did not start successfully, was not opened for writing or a write_pipe was already created");
        goto fail0;
    }
    
    // init connection
    if (!BConnection_Init(&o->connection, BConnection_source_pipe(pinst->write_fd), i->params->iparams->reactor, o, write_pipe_connection_handler)) {
        ModuleLog(i, BLOG_ERROR, "BConnection_Init failed");
        goto fail0;
    }
    
    // init connection send interface
    BConnection_SendAsync_Init(&o->connection);
    
    // set send done callback
    StreamPassInterface_Sender_Init(BConnection_SendAsync_GetIf(&o->connection), write_pipe_send_handler_done, o);
    
    // set variables
    o->state = WRITER_STATE_RUNNING;
    o->write_fd = pinst->write_fd;
    o->write_inst = NULL;
    
    // steal write fd from process instance
    pinst->write_fd = -1;
    
    // go up
    NCDModuleInst_Backend_Up(i);
    return;
    
fail0:
    NCDModuleInst_Backend_DeadError(i);
}

static void write_pipe_func_die (void *vo)
{
    struct write_pipe_instance *o = vo;
    
    // free connection resources
    if (o->state == WRITER_STATE_RUNNING) {
        write_pipe_free_connection(o);
    }
    
    NCDModuleInst_Backend_Dead(o->i);
}

static int write_pipe_func_getvar (void *vo, NCD_string_id_t name, NCDValMem *mem, NCDValRef *out)
{
    struct write_pipe_instance *o = vo;
    
    if (name == NCD_STRING_IS_ERROR) {
        int is_error = (o->state == WRITER_STATE_ERROR);
        *out = ncd_make_boolean(mem, is_error, o->i->params->iparams->string_index);
        return 1;
    }
    
    return 0;
}

static void write_func_new (void *vo, NCDModuleInst *i, const struct NCDModuleInst_new_params *params)
{
    struct write_instance *o = vo;
    o->i = i;
    
    NCDValRef data_arg;
    if (!NCDVal_ListRead(params->args, 1, &data_arg)) {
        ModuleLog(i, BLOG_ERROR, "wrong arity");
        goto fail0;
    }
    if (!NCDVal_IsString(data_arg)) {
        ModuleLog(i, BLOG_ERROR, "wrong type");
        goto fail0;
    }
    
    struct write_pipe_instance *write_pipe_inst = params->method_user;
    
    // check if a write error has already occured
    if (write_pipe_inst->state == WRITER_STATE_ERROR) {
        ModuleLog(i, BLOG_ERROR, "write() is disallowed after a write error has occured");
        goto fail0;
    }
    
    // check if the write_pipe has been aborted
    if (write_pipe_inst->state == WRITER_STATE_ABORTED) {
        ModuleLog(i, BLOG_ERROR, "write() is disallowed after a write() has been aborted");
        goto fail0;
    }
    
    // check if the write_pipe has been aborted
    if (write_pipe_inst->state == WRITER_STATE_CLOSED) {
        ModuleLog(i, BLOG_ERROR, "write() is disallowed after close() has been called");
        goto fail0;
    }
    
    ASSERT(write_pipe_inst->state == WRITER_STATE_RUNNING)
    
    // check if there's already a write in progress
    if (write_pipe_inst->write_inst) {
        ModuleLog(i, BLOG_ERROR, "write() is disallowed while another write() is in progress");
        goto fail0;
    }
    
    // initialize write progress state
    o->cstr = NCDVal_StringCstring(data_arg);
    o->pos = 0;
    
    // if there's nothing to send, go up immediately
    if (o->cstr.length == 0) {
        o->write_pipe_inst = NULL;
        NCDModuleInst_Backend_Up(i);
        return;
    }
    
    // set write_pipe
    o->write_pipe_inst = write_pipe_inst;
    
    // register write in write_pipe
    write_pipe_inst->write_inst = o;
    
    // start send operation
    size_t chunk_length;
    const char *chunk_data = b_cstring_get(o->cstr, o->pos, o->cstr.length - o->pos, &chunk_length);
    size_t to_send = (chunk_length > INT_MAX ? INT_MAX : chunk_length);
    StreamPassInterface_Sender_Send(BConnection_SendAsync_GetIf(&write_pipe_inst->connection), (uint8_t *)chunk_data, to_send);
    return;
    
fail0:
    NCDModuleInst_Backend_DeadError(i);
}

static void write_func_die (void *vo)
{
    struct write_instance *o = vo;
    
    // if we're sending, abort write_pipe
    if (o->write_pipe_inst) {
        ASSERT(o->write_pipe_inst->state == WRITER_STATE_RUNNING)
        ASSERT(o->write_pipe_inst->write_inst == o)
        write_pipe_abort(o->write_pipe_inst);
    }
    
    NCDModuleInst_Backend_Dead(o->i);
}

static void close_func_new (void *vo, NCDModuleInst *i, const struct NCDModuleInst_new_params *params)
{
    if (!NCDVal_ListRead(params->args, 0)) {
        ModuleLog(i, BLOG_ERROR, "wrong arity");
        goto fail0;
    }
    
    struct write_pipe_instance *write_pipe_inst = params->method_user;
    
    // check if a write error has already occured
    if (write_pipe_inst->state == WRITER_STATE_ERROR) {
        ModuleLog(i, BLOG_ERROR, "close() is disallowed after a write error has occured");
        goto fail0;
    }
    
    // check if the write_pipe has been aborted
    if (write_pipe_inst->state == WRITER_STATE_ABORTED) {
        ModuleLog(i, BLOG_ERROR, "close() is disallowed after a write() has been aborted");
        goto fail0;
    }
    
    // check if the write_pipe has been closed
    if (write_pipe_inst->state == WRITER_STATE_CLOSED) {
        ModuleLog(i, BLOG_ERROR, "close() is disallowed after close() has been called");
        goto fail0;
    }
    
    // close
    write_pipe_close(write_pipe_inst);
    
    // go up
    NCDModuleInst_Backend_Up(i);
    return;
    
fail0:
    NCDModuleInst_Backend_DeadError(i);
}

static struct NCDModule modules[] = {
    {
        .type = "sys.start_process",
        .func_new2 = process_func_new,
        .func_die = process_func_die,
        .func_getvar2 = process_func_getvar,
        .alloc_size = sizeof(struct process_instance),
        .flags = NCDMODULE_FLAG_ACCEPT_NON_CONTINUOUS_STRINGS
    }, {
        .type = "sys.start_process::wait",
        .func_new2 = wait_func_new,
        .func_die = wait_func_die,
        .func_getvar2 = wait_func_getvar,
        .alloc_size = sizeof(struct wait_instance),
        .flags = NCDMODULE_FLAG_ACCEPT_NON_CONTINUOUS_STRINGS
    }, {
        .type = "sys.start_process::terminate",
        .func_new2 = terminate_func_new,
        .flags = NCDMODULE_FLAG_ACCEPT_NON_CONTINUOUS_STRINGS
    }, {
        .type = "sys.start_process::kill",
        .func_new2 = kill_func_new,
        .flags = NCDMODULE_FLAG_ACCEPT_NON_CONTINUOUS_STRINGS
    }, {
        .type = "sys.start_process::read_pipe",
        .func_new2 = read_pipe_func_new,
        .func_die = read_pipe_func_die,
        .func_getvar2 = read_pipe_func_getvar,
        .alloc_size = sizeof(struct read_pipe_instance),
        .flags = NCDMODULE_FLAG_ACCEPT_NON_CONTINUOUS_STRINGS
    }, {
        .type = "sys.start_process::read_pipe::read",
        .func_new2 = read_func_new,
        .func_die = read_func_die,
        .func_getvar2 = read_func_getvar,
        .alloc_size = sizeof(struct read_instance),
        .flags = NCDMODULE_FLAG_ACCEPT_NON_CONTINUOUS_STRINGS
    }, {
        .type = "sys.start_process::write_pipe",
        .func_new2 = write_pipe_func_new,
        .func_die = write_pipe_func_die,
        .func_getvar2 = write_pipe_func_getvar,
        .alloc_size = sizeof(struct write_pipe_instance),
        .flags = NCDMODULE_FLAG_ACCEPT_NON_CONTINUOUS_STRINGS
    }, {
        .type = "sys.start_process::write_pipe::write",
        .func_new2 = write_func_new,
        .func_die = write_func_die,
        .alloc_size = sizeof(struct write_instance),
        .flags = NCDMODULE_FLAG_ACCEPT_NON_CONTINUOUS_STRINGS
    }, {
        .type = "sys.start_process::write_pipe::close",
        .func_new2 = close_func_new,
        .flags = NCDMODULE_FLAG_ACCEPT_NON_CONTINUOUS_STRINGS
    }, {
        .type = NULL
    }
};

const struct NCDModuleGroup ncdmodule_sys_start_process = {
    .modules = modules
};
