Fork fcgi handler for streams

This commit is contained in:
Olof hagsand 2018-11-04 14:46:15 +01:00
parent 911e6e6ed3
commit f2810e849f
10 changed files with 208 additions and 53 deletions

View file

@ -28,6 +28,7 @@
* Restconf stream notification support according to RFC8040 * Restconf stream notification support according to RFC8040
* See (apps/restconf/README.md) for more details. * See (apps/restconf/README.md) for more details.
* start-time and stop-time query parameters * start-time and stop-time query parameters
* Fork fcgi handler for streams
* Set access/subscribe base URL with: CLICON_STREAM_URL (default "https://localhost") and CLICON_STREAM_PATH (default "streams") * Set access/subscribe base URL with: CLICON_STREAM_URL (default "https://localhost") and CLICON_STREAM_PATH (default "streams")
* Example: new stream "foo" will get access URL: https://localhost/streams/foo * Example: new stream "foo" will get access URL: https://localhost/streams/foo
* Alternative variant using pub/sub support enabled by ./configure --enable-publish * Alternative variant using pub/sub support enabled by ./configure --enable-publish

View file

@ -54,7 +54,7 @@
/* clicon */ /* clicon */
#include <clixon/clixon.h> #include <clixon/clixon.h>
#include <fcgi_stdio.h> /* Need to be after clixon_xml-h due to attribute format */ #include <fcgiapp.h> /* Need to be after clixon_xml-h due to attribute format */
#include "restconf_lib.h" #include "restconf_lib.h"
@ -485,3 +485,23 @@ api_return_err(clicon_handle h,
cbuf_free(cb); cbuf_free(cb);
return retval; return retval;
} }
int
restconf_terminate(clicon_handle h)
{
yang_spec *yspec;
cxobj *x;
clixon_plugin_exit(h);
rpc_callback_delete_all();
clicon_rpc_close_session(h);
if ((yspec = clicon_dbspec_yang(h)) != NULL)
yspec_free(yspec);
if ((yspec = clicon_config_yang(h)) != NULL)
yspec_free(yspec);
if ((x = clicon_conf_xml(h)) != NULL)
xml_free(x);
clicon_handle_exit(h);
clicon_log_exit();
return 0;
}

View file

@ -62,5 +62,6 @@ cbuf *readdata(FCGX_Request *r);
int get_user_cookie(char *cookiestr, char *attribute, char **val); int get_user_cookie(char *cookiestr, char *attribute, char **val);
int api_return_err(clicon_handle h, FCGX_Request *r, cxobj *xerr, int api_return_err(clicon_handle h, FCGX_Request *r, cxobj *xerr,
int pretty, int use_xml); int pretty, int use_xml);
int restconf_terminate(clicon_handle h);
#endif /* _RESTCONF_LIB_H_ */ #endif /* _RESTCONF_LIB_H_ */

View file

@ -73,7 +73,7 @@
/* clicon */ /* clicon */
#include <clixon/clixon.h> #include <clixon/clixon.h>
#include <fcgi_stdio.h> /* Need to be after clixon_xml.h due to attribute format */ #include <fcgiapp.h> /* Need to be after clixon_xml.h due to attribute format */
/* restconf */ /* restconf */
#include "restconf_lib.h" #include "restconf_lib.h"
@ -443,27 +443,8 @@ api_restconf(clicon_handle h,
return retval; return retval;
} }
static int
restconf_terminate(clicon_handle h)
{
yang_spec *yspec;
cxobj *x;
clixon_plugin_exit(h); /* Need global variable to for signal handler XXX */
rpc_callback_delete_all();
clicon_rpc_close_session(h);
if ((yspec = clicon_dbspec_yang(h)) != NULL)
yspec_free(yspec);
if ((yspec = clicon_config_yang(h)) != NULL)
yspec_free(yspec);
if ((x = clicon_conf_xml(h)) != NULL)
xml_free(x);
clicon_handle_exit(h);
clicon_log_exit();
return 0;
}
/* Need global variable to for signal handler */
static clicon_handle _CLICON_HANDLE = NULL; static clicon_handle _CLICON_HANDLE = NULL;
/*! Signall terminates process /*! Signall terminates process
@ -478,12 +459,24 @@ restconf_sig_term(int arg)
__PROGRAM__, __FUNCTION__, getpid(), arg); __PROGRAM__, __FUNCTION__, getpid(), arg);
else else
exit(-1); exit(-1);
if (_CLICON_HANDLE) if (_CLICON_HANDLE){
stream_child_freeall(_CLICON_HANDLE);
restconf_terminate(_CLICON_HANDLE); restconf_terminate(_CLICON_HANDLE);
}
clicon_exit_set(); /* checked in event_loop() */ clicon_exit_set(); /* checked in event_loop() */
exit(-1); exit(-1);
} }
static void
restconf_sig_child(int arg)
{
int status;
int pid;
if ((pid = waitpid(-1, &status, 0)) != -1 && WIFEXITED(status))
stream_child_free(_CLICON_HANDLE, pid);
}
/*! Usage help routine /*! Usage help routine
* @param[in] argv0 command line * @param[in] argv0 command line
* @param[in] h Clicon handle * @param[in] h Clicon handle
@ -532,6 +525,7 @@ main(int argc,
yang_spec *yspecfg = NULL; /* For config XXX clixon bug */ yang_spec *yspecfg = NULL; /* For config XXX clixon bug */
char *yang_filename = NULL; char *yang_filename = NULL;
char *stream_path; char *stream_path;
int finish;
/* In the startup, logs to stderr & debug flag set later */ /* In the startup, logs to stderr & debug flag set later */
clicon_log_init(__PROGRAM__, LOG_INFO, logdst); clicon_log_init(__PROGRAM__, LOG_INFO, logdst);
@ -579,6 +573,11 @@ main(int argc,
clicon_err(OE_DEMON, errno, "Setting signal"); clicon_err(OE_DEMON, errno, "Setting signal");
goto done; goto done;
} }
if (set_signal(SIGCHLD, restconf_sig_child, NULL) < 0){
clicon_err(OE_DEMON, errno, "Setting signal");
goto done;
}
/* Create configure yang-spec */ /* Create configure yang-spec */
if ((yspecfg = yspec_new()) == NULL) if ((yspecfg = yspec_new()) == NULL)
goto done; goto done;
@ -663,11 +662,12 @@ main(int argc,
clixon_plugin_start(h, argc+1, argv-1); clixon_plugin_start(h, argc+1, argv-1);
*(argv-1) = tmp; *(argv-1) = tmp;
/**/
if ((sockpath = clicon_option_str(h, "CLICON_RESTCONF_PATH")) == NULL){ if ((sockpath = clicon_option_str(h, "CLICON_RESTCONF_PATH")) == NULL){
clicon_err(OE_CFG, errno, "No CLICON_RESTCONF_PATH in clixon configure file"); clicon_err(OE_CFG, errno, "No CLICON_RESTCONF_PATH in clixon configure file");
goto done; goto done;
} }
if (FCGX_Init() != 0){ if (FCGX_Init() != 0){ /* How to cleanup memory after this? */
clicon_err(OE_CFG, errno, "FCGX_Init"); clicon_err(OE_CFG, errno, "FCGX_Init");
goto done; goto done;
} }
@ -676,12 +676,13 @@ main(int argc,
clicon_err(OE_CFG, errno, "FCGX_OpenSocket"); clicon_err(OE_CFG, errno, "FCGX_OpenSocket");
goto done; goto done;
} }
if (FCGX_InitRequest(r, sock, 0) != 0){ if (FCGX_InitRequest(r, sock, 0) != 0){
clicon_err(OE_CFG, errno, "FCGX_InitRequest"); clicon_err(OE_CFG, errno, "FCGX_InitRequest");
goto done; goto done;
} }
while (1) { while (1) {
finish = 1; /* If zero, dont finish request, initiate new */
if (FCGX_Accept_r(r) < 0) { if (FCGX_Accept_r(r) < 0) {
clicon_err(OE_CFG, errno, "FCGX_Accept_r"); clicon_err(OE_CFG, errno, "FCGX_Accept_r");
goto done; goto done;
@ -692,7 +693,7 @@ main(int argc,
if (strncmp(path, "/" RESTCONF_API, strlen("/" RESTCONF_API)) == 0) if (strncmp(path, "/" RESTCONF_API, strlen("/" RESTCONF_API)) == 0)
api_restconf(h, r); /* This is the function */ api_restconf(h, r); /* This is the function */
else if (strncmp(path+1, stream_path, strlen(stream_path)) == 0) { else if (strncmp(path+1, stream_path, strlen(stream_path)) == 0) {
api_stream(h, r, stream_path); api_stream(h, r, stream_path, &finish);
} }
else if (strncmp(path, RESTCONF_WELL_KNOWN, strlen(RESTCONF_WELL_KNOWN)) == 0) { else if (strncmp(path, RESTCONF_WELL_KNOWN, strlen(RESTCONF_WELL_KNOWN)) == 0) {
api_well_known(h, r); /* */ api_well_known(h, r); /* */
@ -704,10 +705,19 @@ main(int argc,
} }
else else
clicon_debug(1, "NULL URI"); clicon_debug(1, "NULL URI");
if (finish)
FCGX_Finish_r(r); FCGX_Finish_r(r);
else{ /* A handler is forked so we initiate a new request after instead
of finnishing the old */
if (FCGX_InitRequest(r, sock, 0) != 0){
clicon_err(OE_CFG, errno, "FCGX_InitRequest");
goto done;
}
}
} }
retval = 0; retval = 0;
done: done:
stream_child_freeall(h);
restconf_terminate(h); restconf_terminate(h);
return retval; return retval;
} }

View file

@ -118,7 +118,7 @@ Mapping netconf error-tag -> status code
/* clicon */ /* clicon */
#include <clixon/clixon.h> #include <clixon/clixon.h>
#include <fcgi_stdio.h> /* Need to be after clixon_xml-h due to attribute format */ #include <fcgiapp.h> /* Need to be after clixon_xml-h due to attribute format */
#include "restconf_lib.h" #include "restconf_lib.h"
#include "restconf_methods.h" #include "restconf_methods.h"

View file

@ -82,11 +82,71 @@
/* clicon */ /* clicon */
#include <clixon/clixon.h> #include <clixon/clixon.h>
#include <fcgi_stdio.h> /* Need to be after clixon_xml.h due to attribute format */ #include <fcgiapp.h> /* Need to be after clixon_xml.h due to attribute format */
#include "restconf_lib.h" #include "restconf_lib.h"
#include "restconf_stream.h" #include "restconf_stream.h"
/*
* Constants
*/
/* Enable for forking stream subscription loop.
* Disable to get single threading but blocking on streams
*/
#define STREAM_FORK 1
/* Keep track of children - whjen they exit - their FCGX handle needs to be
* freed with FCGX_Free(&rbk, 0);
*/
struct stream_child{
qelem_t sc_q; /* queue header */
int sc_pid; /* Child process id */
FCGX_Request sc_r; /* FCGI stream data */
};
/* Linked list of children
* @note could hang STREAM_CHILD list on clicon handle instead.
*/
static struct stream_child *STREAM_CHILD = NULL;
/*! Find restconf child using PID and cleanup FCGI Request data
* @param[in] h Clicon handle
* @param[in] pid Process id of child
* @note could hang STREAM_CHILD list on clicon handle instead.
*/
int
stream_child_free(clicon_handle h,
int pid)
{
struct stream_child *sc;
if ((sc = STREAM_CHILD) != NULL){
do {
if (pid == sc->sc_pid){
DELQ(sc, STREAM_CHILD, struct stream_child *);
FCGX_Free(&sc->sc_r, 0);
free(sc);
goto done;
}
sc = NEXTQ(struct stream_child *, sc);
} while (sc && sc != STREAM_CHILD);
}
done:
return 0;
}
int
stream_child_freeall(clicon_handle h)
{
struct stream_child *sc;
while ((sc = STREAM_CHILD) != NULL){
DELQ(sc, STREAM_CHILD, struct stream_child *);
FCGX_Free(&sc->sc_r, 1);
free(sc);
}
return 0;
}
/*! Callback when stream notifications arrive from backend /*! Callback when stream notifications arrive from backend
*/ */
static int static int
@ -278,7 +338,8 @@ stream_timeout(int s,
int int
api_stream(clicon_handle h, api_stream(clicon_handle h,
FCGX_Request *r, FCGX_Request *r,
char *streampath) char *streampath,
int *finish)
{ {
int retval = -1; int retval = -1;
char *path; char *path;
@ -298,6 +359,10 @@ api_stream(clicon_handle h,
cxobj *xret = NULL; cxobj *xret = NULL;
cxobj *xerr; cxobj *xerr;
int s=-1; int s=-1;
#ifdef STREAM_FORK
int pid;
struct stream_child *sc;
#endif
clicon_debug(1, "%s", __FUNCTION__); clicon_debug(1, "%s", __FUNCTION__);
path = FCGX_GetParam("DOCUMENT_URI", r->envp); path = FCGX_GetParam("DOCUMENT_URI", r->envp);
@ -362,6 +427,23 @@ api_stream(clicon_handle h,
if (restconf_stream(h, r, method, qvec, pretty, use_xml, &s) < 0) if (restconf_stream(h, r, method, qvec, pretty, use_xml, &s) < 0)
goto done; goto done;
if (s != -1){ if (s != -1){
#ifdef STREAM_FORK
if ((pid = fork()) == 0){ /* child */
if (pvec)
free(pvec);
if (dvec)
cvec_free(dvec);
if (qvec)
cvec_free(qvec);
if (pcvec)
cvec_free(pcvec);
if (cb)
cbuf_free(cb);
if (cbret)
cbuf_free(cbret);
if (xret)
xml_free(xret);
#endif /* STREAM_FORK */
/* Listen to backend socket */ /* Listen to backend socket */
if (event_reg_fd(s, if (event_reg_fd(s,
restconf_stream_cb, restconf_stream_cb,
@ -379,7 +461,30 @@ api_stream(clicon_handle h,
event_loop(); event_loop();
close(s); close(s);
event_unreg_fd(s, restconf_stream_cb); event_unreg_fd(s, restconf_stream_cb);
event_unreg_fd(r->listen_sock, restconf_stream_cb);
event_unreg_timeout(stream_timeout, (void*)r);
clicon_exit_reset(); clicon_exit_reset();
#ifdef STREAM_FORK
FCGX_Finish_r(r);
FCGX_Free(r, 0);
fprintf(stderr, "child exit and free\n");
restconf_terminate(h);
exit(0);
}
/* parent */
/* Create stream_child struct and store pid and FCGI data, when child
* killed, call FCGX_Free
*/
if ((sc = malloc(sizeof(struct stream_child))) == NULL){
clicon_err(OE_XML, errno, "malloc");
goto done;
}
memset(sc, 0, sizeof(struct stream_child));
sc->sc_pid = pid;
sc->sc_r = *r;
ADDQ(sc, STREAM_CHILD);
*finish = 0; /* If spawn child, we should not finish this stream */
#endif /* STREAM_FORK */
} }
ok: ok:
retval = 0; retval = 0;
@ -399,6 +504,5 @@ api_stream(clicon_handle h,
cbuf_free(cbret); cbuf_free(cbret);
if (xret) if (xret)
xml_free(xret); xml_free(xret);
return retval; return retval;
} }

View file

@ -39,6 +39,8 @@
/* /*
* Prototypes * Prototypes
*/ */
int api_stream(clicon_handle h, FCGX_Request *r, char *streampath); int stream_child_free(clicon_handle h, int pid);
int stream_child_freeall(clicon_handle h);
int api_stream(clicon_handle h, FCGX_Request *r, char *streampath, int *finish);
#endif /* _RESTCONF_STREAM_H_ */ #endif /* _RESTCONF_STREAM_H_ */

View file

@ -39,7 +39,7 @@
#include <errno.h> #include <errno.h>
#include <ctype.h> #include <ctype.h>
#include <signal.h> #include <signal.h>
#include <fcgi_stdio.h> #include <fcgiapp.h>
#include <curl/curl.h> #include <curl/curl.h>
/* cligen */ /* cligen */

View file

@ -18,7 +18,7 @@ clixon_netconf=clixon_netconf
# How to run restconf stand-alone and using valgrind # How to run restconf stand-alone and using valgrind
#sudo su -c "/www-data/clixon_restconf -f $cfg -D 1" -s /bin/sh www-data #sudo su -c "/www-data/clixon_restconf -f $cfg -D 1" -s /bin/sh www-data
#sudo su -c "valgrind --leak-check=full --show-leak-kinds=all /www-data/clixon_restconf -f $cfg -D 1" -s /bin/sh www-data #sudo su -c "valgrind --trace-children=no --child-silent-after-fork=yes --leak-check=full --show-leak-kinds=all /www-data/clixon_restconf -f $cfg -D 1" -s /bin/sh www-data
#clixon_backend="valgrind --leak-check=full --show-leak-kinds=all clixon_backend" #clixon_backend="valgrind --leak-check=full --show-leak-kinds=all clixon_backend"
clixon_backend=clixon_backend clixon_backend=clixon_backend

View file

@ -111,7 +111,8 @@ new "kill old restconf daemon"
sudo pkill -u www-data clixon_restconf sudo pkill -u www-data clixon_restconf
new "start restconf daemon" new "start restconf daemon"
sudo start-stop-daemon -S -q -o -b -x /www-data/clixon_restconf -d /www-data -c www-data -- -f $cfg -y $fyang # -D 1
sudo su -c "/www-data/clixon_restconf -f $cfg -y $fyang" -s /bin/sh www-data &
sleep 2 sleep 2
@ -167,7 +168,6 @@ sleep 2
new "restconf monitor event nonexist stream" new "restconf monitor event nonexist stream"
expectwait 'curl -s -X GET -H "Accept: text/event-stream" -H "Cache-Control: no-cache" -H "Connection: keep-alive" http://localhost/streams/NOTEXIST' 0 '<errors xmlns="urn:ietf:params:xml:ns:yang:ietf-restconf"><error><error-tag>invalid-value</error-tag><error-type>application</error-type><error-severity>error</error-severity><error-message>No such stream</error-message></error></errors>' 2 expectwait 'curl -s -X GET -H "Accept: text/event-stream" -H "Cache-Control: no-cache" -H "Connection: keep-alive" http://localhost/streams/NOTEXIST' 0 '<errors xmlns="urn:ietf:params:xml:ns:yang:ietf-restconf"><error><error-tag>invalid-value</error-tag><error-type>application</error-type><error-severity>error</error-severity><error-message>No such stream</error-message></error></errors>' 2
# 2a) start subscription 8s - expect 1-2 notifications # 2a) start subscription 8s - expect 1-2 notifications
new "2a) start subscriptions 8s - expect 1-2 notifications" new "2a) start subscriptions 8s - expect 1-2 notifications"
ret=$($UTIL -u http://localhost/streams/EXAMPLE -t 8) ret=$($UTIL -u http://localhost/streams/EXAMPLE -t 8)
@ -236,6 +236,23 @@ if [ $nr -lt 10 -o $nr -gt 14 ]; then
err 10 "$nr" err 10 "$nr"
fi fi
# Try parallell
# start background job
curl -s -X GET -H "Accept: text/event-stream" -H "Cache-Control: no-cache" -H "Connection: keep-alive" "http://localhost/streams/EXAMPLE" > /dev/null &
new "Start subscription in parallell"
ret=$($UTIL -u http://localhost/streams/EXAMPLE -t 8)
expect="data: <notification xmlns=\"urn:ietf:params:xml:ns:netconf:notification:1.0\"><eventTime>${DATE}T[0-9:.]*</eventTime><event><event-class>fault</event-class><reportingEntity><card>Ethernet0</card></reportingEntity><severity>major</severity></event>"
match=$(echo "$ret" | grep -Eo "$expect")
if [ -z "$match" ]; then
err "$expect" "$ret"
fi
nr=$(echo "$ret" | grep -c "data:")
if [ $nr -lt 1 -o $nr -gt 2 ]; then
err 2 "$nr"
fi
#----------------- #-----------------
sudo pkill -u www-data clixon_restconf sudo pkill -u www-data clixon_restconf