347fa1a7a5
This cuts down on the boilerplate especially with ncd_make_boolean().
1250 lines
37 KiB
C
1250 lines
37 KiB
C
/**
|
|
* @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/extra/NCDBuf.h>
|
|
#include <ncd/extra/build_cmdline.h>
|
|
#include <ncd/extra/NCDBProcessOpts.h>
|
|
|
|
#include <ncd/module_common.h>
|
|
|
|
#include <generated/blog_channel_ncd_sys_start_process.h>
|
|
|
|
#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;
|
|
MemRef data;
|
|
};
|
|
|
|
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;
|
|
|
|
MEMREF_LOOP_CHARS(NCDVal_StringMemRef(mode_arg), 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")) {
|
|
if (!ncd_read_boolean(val, &o->term_on_deinit)) {
|
|
ModuleLog(o->i, BLOG_ERROR, "term_on_deinit: bad value");
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
if (NCDVal_IsString(key) && NCDVal_StringEquals(key, "deinit_kill_time")) {
|
|
if (NCDVal_IsString(val) && NCDVal_StringEquals(val, "never")) {
|
|
o->deinit_kill_time = -2;
|
|
}
|
|
else if (NCDVal_IsString(val) && NCDVal_StringEqualsId(val, NCD_STRING_EMPTY)) {
|
|
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);
|
|
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, 0), 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);
|
|
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);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
if (name == NCD_STRING_NOT_EOF) {
|
|
int not_eof = (o->read_size > 0);
|
|
*out = ncd_make_boolean(mem, not_eof);
|
|
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->data.len)
|
|
|
|
struct write_instance *wr = o->write_inst;
|
|
|
|
// update write progress
|
|
wr->data = MemRef_SubFrom(wr->data, data_len);
|
|
|
|
// if there is more data, start another write operation
|
|
if (wr->data.len > 0) {
|
|
size_t to_send = (wr->data.len > INT_MAX) ? INT_MAX : wr->data.len;
|
|
StreamPassInterface_Sender_Send(BConnection_SendAsync_GetIf(&o->connection), (uint8_t *)wr->data.ptr, 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, 0), 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);
|
|
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->data = NCDVal_StringMemRef(data_arg);
|
|
|
|
// if there's nothing to send, go up immediately
|
|
if (o->data.len == 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 to_send = (o->data.len > INT_MAX) ? INT_MAX : o->data.len;
|
|
StreamPassInterface_Sender_Send(BConnection_SendAsync_GetIf(&write_pipe_inst->connection), (uint8_t *)o->data.ptr, 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)
|
|
}, {
|
|
.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)
|
|
}, {
|
|
.type = "sys.start_process::terminate",
|
|
.func_new2 = terminate_func_new
|
|
}, {
|
|
.type = "sys.start_process::kill",
|
|
.func_new2 = kill_func_new
|
|
}, {
|
|
.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)
|
|
}, {
|
|
.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)
|
|
}, {
|
|
.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)
|
|
}, {
|
|
.type = "sys.start_process::write_pipe::write",
|
|
.func_new2 = write_func_new,
|
|
.func_die = write_func_die,
|
|
.alloc_size = sizeof(struct write_instance)
|
|
}, {
|
|
.type = "sys.start_process::write_pipe::close",
|
|
.func_new2 = close_func_new
|
|
}, {
|
|
.type = NULL
|
|
}
|
|
};
|
|
|
|
const struct NCDModuleGroup ncdmodule_sys_start_process = {
|
|
.modules = modules
|
|
};
|