From 94c6bd9c49db886e3828f35af583931e4f175ad7 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Fri, 22 Jan 2021 14:41:47 +0100 Subject: [PATCH] Added proc socket and text proto rpc --- docker/main/startsystem_evhtp.sh | 2 +- docker/main/startsystem_fcgi.sh | 2 +- lib/clixon/clixon_handle.h | 1 + lib/clixon/clixon_proc.h | 4 +- lib/clixon/clixon_proto.h | 8 +- lib/src/clixon_proc.c | 203 +++++++--------------- lib/src/clixon_proto.c | 280 +++++++++++++++---------------- lib/src/clixon_proto_client.c | 2 +- util/clixon_util_socket.c | 2 +- 9 files changed, 214 insertions(+), 290 deletions(-) diff --git a/docker/main/startsystem_evhtp.sh b/docker/main/startsystem_evhtp.sh index 369f2c37..2729c0c2 100755 --- a/docker/main/startsystem_evhtp.sh +++ b/docker/main/startsystem_evhtp.sh @@ -64,7 +64,7 @@ echo "$STORE" > /usr/local/var/example/running_db # - test_install.sh since you dont have the make environment cat < /usr/local/bin/test/site.sh # Add your local site specific env variables (or tests) here. -SKIPLIST="test_api.sh test_c++.sh test_yangmodels.sh test_openconfig.sh test_install.sh test_privileges.sh" +SKIPLIST="test_api.sh test_client.sh test_c++.sh test_yangmodels.sh test_openconfig.sh test_install.sh test_privileges.sh" RCPROTO=https #IETFRFC= EOF diff --git a/docker/main/startsystem_fcgi.sh b/docker/main/startsystem_fcgi.sh index 71835a24..362bc926 100755 --- a/docker/main/startsystem_fcgi.sh +++ b/docker/main/startsystem_fcgi.sh @@ -88,7 +88,7 @@ EOF # - test_order.sh XXX this is a bug need debugging cat < /usr/local/bin/test/site.sh # Add your local site specific env variables (or tests) here. -SKIPLIST="test_api.sh test_c++.sh test_yangmodels.sh test_openconfig.sh test_install.sh test_privileges.sh" +SKIPLIST="test_api.sh test_client.sh test_c++.sh test_yangmodels.sh test_openconfig.sh test_install.sh test_privileges.sh" #IETFRFC= EOF diff --git a/lib/clixon/clixon_handle.h b/lib/clixon/clixon_handle.h index 807115e0..1f6b54c6 100644 --- a/lib/clixon/clixon_handle.h +++ b/lib/clixon/clixon_handle.h @@ -46,6 +46,7 @@ Note that its contents is different dependending on if invoked from a cli/backend/netconf or other plugin. But this is hidden under-the-hood. */ +typedef void *clixon_handle; typedef void *clicon_handle; /* The dynamicically loadable plugin object handle (should be in clixon_plugin.h) */ diff --git a/lib/clixon/clixon_proc.h b/lib/clixon/clixon_proc.h index 98c4ab21..68770ef5 100644 --- a/lib/clixon/clixon_proc.h +++ b/lib/clixon/clixon_proc.h @@ -49,9 +49,9 @@ typedef int (proc_cb_t)(clicon_handle h, process_entry_t *pe, char **operation); /* * Prototypes */ -int clixon_proc_run(char **argv, void (outcb)(char *), int doerr); +int clixon_proc_socket(char **argv, pid_t *pid, int *fdin, int *fdout); +int clixon_proc_socket_close(pid_t pid, int fdin, int fdout); int clixon_proc_background(char **argv, const char *netns, pid_t *pid); -int clixon_proc_daemon(char **argv, pid_t *pid); int clixon_process_register(clicon_handle h, const char *name, const char *netns, proc_cb_t *callback, char **argv, int argc); int clixon_process_delete_all(clicon_handle h); int clixon_process_operation(clicon_handle h, const char *name, char *op, const int wrapit, uint32_t *pid); diff --git a/lib/clixon/clixon_proto.h b/lib/clixon/clixon_proto.h index 43ee6575..2eff72bc 100644 --- a/lib/clixon/clixon_proto.h +++ b/lib/clixon/clixon_proto.h @@ -83,12 +83,18 @@ int clicon_rpc_connect_inet(clicon_handle h, uint16_t port, int *sock0); -int clicon_rpc(int s, struct clicon_msg *msg, char **xret); +int clicon_rpc(int fdin, int fdout, struct clicon_msg *msg, char **xret); + +int clicon_rpc1(int fdin, int fdout, cbuf *msgin, cbuf *msgret); int clicon_msg_send(int s, struct clicon_msg *msg); +int clicon_msg_send1(int s, cbuf *cb); + int clicon_msg_rcv(int s, struct clicon_msg **msg, int *eof); +int clicon_msg_rcv1(int s, cbuf *cb, int *eof); + int send_msg_notify_xml(clicon_handle h, int s, cxobj *xev); int send_msg_reply(int s, char *data, uint32_t datalen); diff --git a/lib/src/clixon_proc.c b/lib/src/clixon_proc.c index 7a4061a0..d2a5c3fb 100644 --- a/lib/src/clixon_proc.c +++ b/lib/src/clixon_proc.c @@ -105,35 +105,23 @@ clixon_proc_sigint(int sig) kill(_clicon_proc_child, SIGINT); } -/*! Fork a child process, setup a pipe between parent and child. - * Allowing parent to read the output of the child. - * @param[in] doerr If non-zero, stderr will be directed to the pipe as well. - * The pipe for the parent to write - * to the child is closed and cannot be used. - * - * When child process is done with the pipe setup, execute the specified - * command, execv(argv[0], argv). - * - * When parent is done with the pipe setup it will read output from the child - * until eof. The read output will be sent to the specified output callback, - * 'outcb' function. - * +/*! * @param[in] argv NULL-terminated Argument vector - * @param[in] outcb - * @param[in] doerr - * @retval number Matches (processes affected). + * @param[in] doerr If non-zero, stderr will be directed to the pipe as well. + * @param[out] s Socket + * @retval O OK * @retval -1 Error. + * @note need to cleanup and wait on sub-process */ int -clixon_proc_run(char **argv, - void (outcb)(char *), - int doerr) +clixon_proc_socket(char **argv, + pid_t *pid, + int *fdin, + int *fdout) { int retval = -1; - char buf[512]; - int outfd[2] = { -1, -1 }; - int n; - int status; + int p2c[2] = { -1, -1 }; /* parent->child */ + int c2p[2] = { -1, -1 }; /* child->parent */ pid_t child; sigfn_t oldhandler = NULL; sigset_t oset; @@ -143,66 +131,60 @@ clixon_proc_run(char **argv, clicon_err(OE_UNIX, EINVAL, "argv is NULL"); goto done; } - if (pipe(outfd) == -1){ + if (pipe2(p2c, O_DIRECT) == -1){ /* parent->child */ clicon_err(OE_UNIX, errno, "pipe"); goto done; } + if (pipe2(c2p, O_DIRECT) == -1){ /* child->parent */ + clicon_err(OE_UNIX, errno, "pipe"); + goto done; + } + // clicon_debug(1, "%s p2c: %d -> %d c2p: %d <- %d", __FUNCTION__, p2c[1], p2c[0], c2p[0], c2p[1]); sigprocmask(0, NULL, &oset); set_signal(SIGINT, clixon_proc_sigint, &oldhandler); sig++; - if ((child = fork()) < 0) { clicon_err(OE_UNIX, errno, "fork"); goto done; } - if (child == 0) { /* Child */ - /* Unblock all signals except TSTP */ clicon_signal_unblock(0); signal(SIGTSTP, SIG_IGN); - close(outfd[0]); /* Close unused read ends */ - outfd[0] = -1; + close(0); - /* Divert stdout and stderr to pipes */ - dup2(outfd[1], STDOUT_FILENO); - if (doerr) - dup2(outfd[1], STDERR_FILENO); - - execvp(argv[0], argv); - perror("execvp"); /* Shouldnt reach here */ - exit(-1); - } - - /* Parent */ - - /* Close unused write ends */ - close(outfd[1]); - outfd[1] = -1; - - /* Read from pipe */ - while ((n = read(outfd[0], buf, sizeof(buf)-1)) != 0) { - if (n < 0) { - if (errno == EINTR) - continue; - break; + if (dup2(p2c[0], STDIN_FILENO) < 0){ + // if (dup(p2c[0]) < 0){ + perror("dup2"); + return -1; } - buf[n] = '\0'; - /* Pass read data to callback function is defined */ - if (outcb) - outcb(buf); + close(p2c[1]); + close(p2c[0]); + + close(1); + if (dup2(c2p[1], STDOUT_FILENO) < 0){ + // if (dup(c2p[1]) < 0){ + perror("dup2"); + return -1; + } + close(c2p[1]); + close(c2p[0]); + + if (execvp(argv[0], argv) < 0){ + perror("execvp"); + return -1; + } + exit(-1); /* Shouldnt reach here */ } - - /* Wait for child to finish */ - if(waitpid(child, &status, 0) == child) - retval = WEXITSTATUS(status); + /* Parent */ + close(p2c[0]); + close(c2p[1]); + *pid = child; + *fdout = p2c[1]; + *fdin = c2p[0]; + retval = 0; done: - /* Clean up all pipes */ - if (outfd[0] != -1) - close(outfd[0]); - if (outfd[1] != -1) - close(outfd[1]); if (sig){ /* Restore sigmask and fn */ sigprocmask(SIG_SETMASK, &oset, NULL); set_signal(SIGINT, oldhandler, NULL); @@ -210,6 +192,25 @@ clixon_proc_run(char **argv, return retval; } +int +clixon_proc_socket_close(pid_t pid, + int fdin, + int fdout) +{ + int retval = -1; + int status; + + if (fdin != -1) + close(fdin); + if (fdout != -1) + close(fdout); /* Usually kills */ + kill(pid, SIGTERM); + // usleep(100000); /* Wait for child to finish */ + if(waitpid(pid, &status, 0) == pid) + retval = WEXITSTATUS(status); + return retval; +} + /*! Fork and exec a sub-process, let it run and return pid * * @param[in] argv NULL-terminated Argument vector @@ -217,7 +218,6 @@ clixon_proc_run(char **argv, * @param[out] pid * @retval 0 OK * @retval -1 Error. - * @see clixon_proc_daemon * @note SIGCHLD is set to IGN here. Maybe it should be done in main? */ int @@ -305,79 +305,6 @@ clixon_proc_background(char **argv, return retval; } -/*! Double fork and exec a sub-process as a daemon, let it run and return pid - * - * @param[in] argv NULL-terminated Argument vector - * @param[out] pid - * @retval 0 OK - * @retval -1 Error. - * @see clixon_proc_background - */ -int -clixon_proc_daemon(char **argv, - pid_t *pid0) -{ - int retval = -1; - int status; - pid_t child; - pid_t pid; - int i; - struct rlimit rlim = {0, }; - FILE *fpid = NULL; - - if (argv == NULL){ - clicon_err(OE_UNIX, EINVAL, "argv is NULL"); - goto done; - } - if ((fpid = tmpfile()) == NULL){ - clicon_err(OE_UNIX, errno, "tmpfile"); - goto done; - } - if ((child = fork()) < 0) { - clicon_err(OE_UNIX, errno, "fork"); - goto done; - } - if (child == 0) { /* Child */ - clicon_signal_unblock(0); - if ((pid = fork()) < 0) { - clicon_err(OE_UNIX, errno, "fork"); - return -1; - } - if (pid == 0) { /* Grandchild, create new session */ - setsid(); - if (chdir("/") < 0){ - clicon_err(OE_UNIX, errno, "chdirq"); - exit(1); - } - /* Close open descriptors */ - if ( ! getrlimit(RLIMIT_NOFILE, &rlim)) - for (i = 0; i < rlim.rlim_cur; i++) - close(i); - if (execv(argv[0], argv) < 0) { - clicon_err(OE_UNIX, errno, "execv"); - exit(1); - } - /* Not reached */ - } - if (fprintf(fpid, "%ld\n", (long) pid) < 1){ - clicon_err(OE_DAEMON, errno, "fprintf Could not write pid"); - goto done; - } - fclose(fpid); - // waitpid(pid, &status2, 0); - exit(pid); - } - - if (waitpid(child, &status, 0) > 0){ - pidfile_get_fd(fpid, pid0); - retval = 0; - } - fclose(fpid); - done: - clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); - return retval; -} - /*--------------------------------------------------------------------------------* * Process management: start/stop registered processes for internal use */ diff --git a/lib/src/clixon_proto.c b/lib/src/clixon_proto.c index 2d6b1a68..d2a23873 100644 --- a/lib/src/clixon_proto.c +++ b/lib/src/clixon_proto.c @@ -67,9 +67,7 @@ /* clicon */ #include "clixon_err.h" #include "clixon_queue.h" -#ifdef CLIXON_PROTO_PLAIN #include "clixon_event.h" -#endif #include "clixon_hash.h" #include "clixon_handle.h" #include "clixon_log.h" @@ -323,7 +321,7 @@ msg_dump(struct clicon_msg *msg) return retval; } -/*! Send a CLICON netconf message +/*! Send a CLICON netconf message using internal IPC message * @param[in] s socket (unix or inet) to communicate with backend * @param[out] msg CLICON msg data reply structure. Free with free() */ @@ -337,20 +335,6 @@ clicon_msg_send(int s, __FUNCTION__, ntohl(msg->op_len)); if (clicon_debug_get() > 2) msg_dump(msg); -#ifdef CLIXON_PROTO_PLAIN - { - cbuf *cb = NULL; - if ((cb = cbuf_new()) == NULL) - goto done; - cprintf(cb, "%s]]>]]>", (char*)&msg->op_body); - if (atomicio((ssize_t (*)(int, void *, size_t))write, - s, cbuf_get(cb), cbuf_len(cb)+1) < 0){ - clicon_err(OE_CFG, errno, "atomicio"); - clicon_log(LOG_WARNING, "%s: write: %s", __FUNCTION__, strerror(errno)); - goto done; - } - } -#else if (atomicio((ssize_t (*)(int, void *, size_t))write, s, msg, ntohl(msg->op_len)) < 0){ clicon_err(OE_CFG, errno, "atomicio"); @@ -358,123 +342,12 @@ clicon_msg_send(int s, strerror(errno), ntohs(msg->op_len), msg->op_body); goto done; } -#endif /* CLIXON_PROTO_PLAIN */ retval = 0; done: return retval; } -#ifdef CLIXON_PROTO_PLAIN -/*! Receive a message using plain ascii - * @see netconf_input_cb() - */ -static int -clicon_msg_rcv1(int s, - cbuf **cb1, - int *eof) -{ - int retval = -1; - unsigned char buf[BUFSIZ]; - int i; - int len; - cbuf *cb=NULL; - int xml_state = 0; - int poll; - - clicon_debug(1, "%s", __FUNCTION__); - if ((cb = cbuf_new()) == NULL){ - clicon_err(OE_XML, errno, "cbuf_new"); - return retval; - } - memset(buf, 0, sizeof(buf)); - while (1){ - if ((len = read(s, buf, sizeof(buf))) < 0){ - if (errno == ECONNRESET) - len = 0; /* emulate EOF */ - else{ - clicon_log(LOG_ERR, "%s: read: %s", __FUNCTION__, strerror(errno)); - goto done; - } - } /* read */ - if (len == 0){ /* EOF */ - // cc_closed++; - close(s); - goto ok; - } - for (i=0; i]]>", - buf[i], - &xml_state)) { - /* OK, we have an xml string from a client */ - /* Remove trailer */ - *(((char*)cbuf_get(cb)) + cbuf_len(cb) - strlen("]]>]]>")) = '\0'; - *cb1 = cb; - clicon_debug(2, "%s", cbuf_get(cb)); - cb = NULL; - goto ok; - } - } - /* poll==1 if more, poll==0 if none */ - if ((poll = clixon_event_poll(s)) < 0) - goto done; - if (poll == 0) - break; /* No data to read */ - } /* while */ - ok: - retval = 0; - done: - clicon_debug(1, "%s done", __FUNCTION__); - if (cb) - cbuf_free(cb); - // if (cc_closed) - // retval = -1; - return retval; -} - -/*! Receive a message using plain ascii - * @note message is copied once too many - * @note session-id is made up - */ -static int -clicon_msg_rcv_plain(int s, - struct clicon_msg **msg0, - int *eof) -{ - int retval = -1; - cbuf *cb = NULL; - size_t sz; - static int ii = 0; - struct clicon_msg *msg = NULL; - - if (clicon_msg_rcv1(s, &cb, eof) < 0) - goto done; - if (cb == NULL){ - clicon_err(OE_CFG, EFAULT, "unrecognized input"); - *eof = 1; - goto ok; - } - sz = sizeof(struct clicon_msg) + cbuf_len(cb) + 1; - if ((msg = (struct clicon_msg *)malloc(sz)) == NULL){ - clicon_err(OE_CFG, errno, "malloc"); - goto done; - } - memset(msg, 0, sz); - msg->op_len = htonl(sz); - msg->op_id = htonl(ii++); /* XXX seesion-ids are randomized */ - strcpy((char*)&msg->op_body, cbuf_get(cb)); /* XXX message data copied */ - cbuf_free(cb); - *msg0 = msg; - ok: - retval = 0; - done: - return retval; -} -#endif /* CLIXON_PROTO_PLAIN */ - -/*! Receive a CLICON message +/*! Receive a CLICON message using IPC message struct * * XXX: timeout? and signals? * There is rudimentary code for turning on signals and handling them @@ -501,12 +374,6 @@ clicon_msg_rcv(int s, sigfn_t oldhandler; uint32_t mlen; -#ifdef CLIXON_PROTO_PLAIN /* for testing, eg fuzzing */ - if (clicon_msg_rcv_plain(s, msg, eof) < 0) - goto done; - return 0; -#endif - *eof = 0; if (0) set_signal(SIGINT, atomicio_sig_handler, &oldhandler); @@ -549,6 +416,90 @@ clicon_msg_rcv(int s, return retval; } +/*! Receive a message using plain ascii + * @param[in] s socket (unix or inet) to communicate with backend + * @param[out] cb1 cligen buf struct containing the incoming message + * @param[out] eof Set if eof encountered + * @see netconf_input_cb() + */ +int +clicon_msg_rcv1(int s, + cbuf *cb, + int *eof) +{ + int retval = -1; + unsigned char buf[BUFSIZ]; + int i; + int len; + int xml_state = 0; + int poll; + + clicon_debug(1, "%s", __FUNCTION__); + memset(buf, 0, sizeof(buf)); + while (1){ + if ((len = read(s, buf, sizeof(buf))) < 0){ + if (errno == ECONNRESET) + len = 0; /* emulate EOF */ + else{ + clicon_log(LOG_ERR, "%s: read: %s errno:%d", __FUNCTION__, strerror(errno), errno); + goto done; + } + } /* read */ + if (len == 0){ /* EOF */ + // cc_closed++; + close(s); + goto ok; + } + for (i=0; i]]>", + buf[i], + &xml_state)) { + /* OK, we have an xml string from a client */ + /* Remove trailer */ + *(((char*)cbuf_get(cb)) + cbuf_len(cb) - strlen("]]>]]>")) = '\0'; + goto ok; + } + } + /* poll==1 if more, poll==0 if none */ + if ((poll = clixon_event_poll(s)) < 0) + goto done; + if (poll == 0) + break; /* No data to read */ + } /* while */ + ok: + retval = 0; + done: + clicon_debug(1, "%s done", __FUNCTION__); + // if (cc_closed) + // retval = -1; + return retval; +} + +/*! Send a CLICON netconf message plain text + * @param[in] s socket (unix or inet) to communicate with backend + * @param[out] msg CLICON msg data reply structure. Free with free() + */ +int +clicon_msg_send1(int s, + cbuf *cb) +{ + int retval = -1; + + cprintf(cb, "]]>]]>"); + if (atomicio((ssize_t (*)(int, void *, size_t))write, + s, cbuf_get(cb), cbuf_len(cb)+1) < 0){ + clicon_err(OE_CFG, errno, "atomicio"); + clicon_log(LOG_WARNING, "%s: write: %s", __FUNCTION__, strerror(errno)); + goto done; + } + retval = 0; + done: + return retval; +} + /*! Connect to server, send a clicon_msg message and wait for result using unix socket * * @param[in] h Clicon handle @@ -593,8 +544,7 @@ clicon_rpc_connect_unix(clicon_handle h, /*! Connect to server, send a clicon_msg message and wait for result using an inet socket * - * @param[in] h Clicon handle - * @param[in] msg CLICON msg data structure. It has fixed header and variable body. + * @param[in] h Clicon handle (not used) * @param[in] dst IPv4 address * @param[in] port TCP port * @param[out] retdata Returned data as string netconf xml tree. @@ -647,14 +597,16 @@ clicon_rpc_connect_inet(clicon_handle h, * errno set to ENOTCONN which means that socket is now closed probably * due to remote peer disconnecting. The caller may have to do something,... * - * @param[in] s Socket to communicate with backend + * @param[in] fdin Input file descriptor + * @param[in] fdout Output file descriptor (for socket same as fdin) * @param[in] msg CLICON msg data structure. It has fixed header and variable body. * @param[out] xret Returned data as netconf xml tree. * @retval 0 OK * @retval -1 Error */ int -clicon_rpc(int s, +clicon_rpc(int fdin, + int fdout, struct clicon_msg *msg, char **ret) { @@ -662,15 +614,14 @@ clicon_rpc(int s, struct clicon_msg *reply = NULL; int eof; char *data = NULL; - cxobj *cx = NULL; - if (clicon_msg_send(s, msg) < 0) + if (clicon_msg_send(fdout, msg) < 0) goto done; - if (clicon_msg_rcv(s, &reply, &eof) < 0) + if (clicon_msg_rcv(fdin, &reply, &eof) < 0) goto done; if (eof){ clicon_err(OE_PROTO, ESHUTDOWN, "Unexpected close of CLICON_SOCK. Clixon backend daemon may have crashed."); - close(s); + close(fdin); /* assume socket */ errno = ESHUTDOWN; goto done; } @@ -682,13 +633,52 @@ clicon_rpc(int s, } retval = 0; done: - if (cx) - xml_free(cx); if (reply) free(reply); return retval; } +/*! Send a netconf message and recevive result. + * + * TBD: timeout, interrupt? + * retval may be -1 and + * errno set to ENOTCONN which means that socket is now closed probably + * due to remote peer disconnecting. The caller may have to do something,... + * + * @param[in] fdin Input file descriptor + * @param[in] fdout Output file descriptor (for socket same as fdin) + * @param[in] msgin CLICON msg data structure. It has fixed header and variable body. + * @param[out] msgret Returned data as netconf xml tree. + * @retval 0 OK + * @retval -1 Error + * see clicon_rpc using clicon_msg + */ +int +clicon_rpc1(int fdin, + int fdout, + cbuf *msg, + cbuf *msgret) +{ + int retval = -1; + int eof; + + clicon_debug(1, "%s", __FUNCTION__); + if (clicon_msg_send1(fdout, msg) < 0) + goto done; + if (clicon_msg_rcv1(fdin, msgret, &eof) < 0) + goto done; + if (eof){ + clicon_err(OE_PROTO, ESHUTDOWN, "Unexpected close of CLICON_SOCK. Clixon backend daemon may have crashed."); + close(fdin); /* assume socket */ + errno = ESHUTDOWN; + goto done; + } + retval = 0; + done: + clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); + return retval; +} + /*! Send a clicon_msg message as reply to a clicon rpc request * * @param[in] s Socket to communicate with client diff --git a/lib/src/clixon_proto_client.c b/lib/src/clixon_proto_client.c index 66741690..07012227 100644 --- a/lib/src/clixon_proto_client.c +++ b/lib/src/clixon_proto_client.c @@ -154,7 +154,7 @@ clicon_rpc_msg(clicon_handle h, /* Create a socket and connect to it, either UNIX, IPv4 or IPv6 per config options */ if (clicon_rpc_connect(h, &s) < 0) goto done; - if (clicon_rpc(s, msg, &retdata) < 0) + if (clicon_rpc(s, s, msg, &retdata) < 0) goto done; clicon_debug(1, "%s retdata:%s", __FUNCTION__, retdata); diff --git a/util/clixon_util_socket.c b/util/clixon_util_socket.c index 75a8992e..038aeacd 100644 --- a/util/clixon_util_socket.c +++ b/util/clixon_util_socket.c @@ -176,7 +176,7 @@ main(int argc, else if (clicon_rpc_connect_inet(h, sockpath, 4535, &s) < 0) goto done; - if (clicon_rpc(s, msg, &retdata) < 0) + if (clicon_rpc(s, s, msg, &retdata) < 0) goto done; close(s); fprintf(stdout, "%s\n", retdata);