blob: eaa3d9f277e3e562961c2922c6b56ec413599766 [file] [log] [blame]
/*
* libwebsockets-test-server - libwebsockets test implementation
*
* Copyright (C) 2010-2011 Andy Green <andy@warmcat.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation:
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <getopt.h>
#include <string.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <sys/queue.h>
#include <fcntl.h>
#include <assert.h>
#include <pthread.h>
#include <errno.h>
#include <libnetconf.h>
#include <libwebsockets.h>
#include "notification_module.h"
#include "mod_netconf.h"
#include "../config.h"
#ifdef TEST_NOTIFICATION_SERVER
static int force_exit = 0;
#endif
#if defined(TEST_NOTIFICATION_SERVER) || defined(WITH_NOTIFICATIONS)
static int max_poll_elements;
static struct pollfd *pollfds;
static int *fd_lookup;
static int count_pollfds;
static struct libwebsocket_context *context = NULL;
struct ntf_thread_config {
struct nc_session *session;
char *session_id;
};
extern struct session_with_mutex *netconf_sessions_list;
static pthread_key_t thread_key;
/*
* This demo server shows how to use libwebsockets for one or more
* websocket protocols in the same server
*
* It defines the following websocket protocols:
*
* dumb-increment-protocol: once the socket is opened, an incrementing
* ascii string is sent down it every 50ms.
* If you send "reset\n" on the websocket, then
* the incrementing number is reset to 0.
*
* lws-mirror-protocol: copies any received packet to every connection also
* using this protocol, including the sender
*/
enum demo_protocols {
/* always first */
PROTOCOL_HTTP = 0,
PROTOCOL_NOTIFICATION,
/* always last */
DEMO_PROTOCOL_COUNT
};
#define LOCAL_RESOURCE_PATH "."
char *resource_path = LOCAL_RESOURCE_PATH;
/*
* We take a strict whitelist approach to stop ../ attacks
*/
struct serveable {
const char *urlpath;
const char *mimetype;
};
static const struct serveable whitelist[] = {
{ "/favicon.ico", "image/x-icon" },
{ "/libwebsockets.org-logo.png", "image/png" },
/* last one is the default served if no match */
{ "/test.html", "text/html" },
};
struct per_session_data__http {
int fd;
};
/* this protocol server (always the first one) just knows how to do HTTP */
static int callback_http(struct libwebsocket_context *context,
struct libwebsocket *wsi,
enum libwebsocket_callback_reasons reason, void *user,
void *in, size_t UNUSED(len))
{
char client_name[128];
char client_ip[128];
char buf[256];
int n, m;
static unsigned char buffer[4096];
struct per_session_data__http *pss = (struct per_session_data__http *)user;
struct libwebsocket_pollargs *pa = (struct libwebsocket_pollargs *) in;
switch (reason) {
case LWS_CALLBACK_HTTP:
for (n = 0; n < (signed) (sizeof(whitelist) / sizeof(whitelist[0]) - 1); n++)
if (in && strcmp((const char *)in, whitelist[n].urlpath) == 0)
break;
sprintf(buf, "%s%s", resource_path, whitelist[n].urlpath);
if (libwebsockets_serve_http_file(context, wsi, buf, whitelist[n].mimetype, NULL, 0))
return -1; /* through completion or error, close the socket */
/*
* notice that the sending of the file completes asynchronously,
* we'll get a LWS_CALLBACK_HTTP_FILE_COMPLETION callback when
* it's done
*/
break;
case LWS_CALLBACK_HTTP_FILE_COMPLETION:
// lwsl_info("LWS_CALLBACK_HTTP_FILE_COMPLETION seen\n");
/* kill the connection after we sent one file */
return -1;
case LWS_CALLBACK_HTTP_WRITEABLE:
/*
* we can send more of whatever it is we were sending
*/
do {
n = read(pss->fd, buffer, sizeof buffer);
/* problem reading, close conn */
if (n < 0)
goto bail;
/* sent it all, close conn */
if (n == 0)
goto bail;
/*
* because it's HTTP and not websocket, don't need to take
* care about pre and postamble
*/
m = libwebsocket_write(wsi, buffer, n, LWS_WRITE_HTTP);
if (m < 0)
/* write failed, close conn */
goto bail;
if (m != n)
/* partial write, adjust */
lseek(pss->fd, m - n, SEEK_CUR);
} while (!lws_send_pipe_choked(wsi));
libwebsocket_callback_on_writable(context, wsi);
break;
bail:
close(pss->fd);
return -1;
case LWS_CALLBACK_LOCK_POLL:
/*
* lock mutex to protect pollfd state
* called before any other POLL related callback
*/
break;
case LWS_CALLBACK_UNLOCK_POLL:
/*
* unlock mutex to protect pollfd state when
* called after any other POLL related callback
*/
break;
/*
* callback for confirming to continue with client IP appear in
* protocol 0 callback since no websocket protocol has been agreed
* yet. You can just ignore this if you won't filter on client IP
* since the default uhandled callback return is 0 meaning let the
* connection continue.
*/
case LWS_CALLBACK_FILTER_NETWORK_CONNECTION:
libwebsockets_get_peer_addresses(context, wsi, (int)(long)in, client_name,
sizeof(client_name), client_ip, sizeof(client_ip));
//fprintf(stderr, "Received network connect from %s (%s)\n", client_name, client_ip);
/* if we returned non-zero from here, we kill the connection */
break;
/*
* callbacks for managing the external poll() array appear in
* protocol 0 callback
*/
case LWS_CALLBACK_ADD_POLL_FD:
if (count_pollfds >= max_poll_elements) {
lwsl_err("LWS_CALLBACK_ADD_POLL_FD: too many sockets to track\n");
return 1;
}
fd_lookup[pa->fd] = count_pollfds;
pollfds[count_pollfds].fd = pa->fd;
pollfds[count_pollfds].events = pa->events;
pollfds[count_pollfds++].revents = 0;
break;
case LWS_CALLBACK_DEL_POLL_FD:
if (!--count_pollfds)
break;
m = fd_lookup[pa->fd];
/* have the last guy take up the vacant slot */
pollfds[m] = pollfds[count_pollfds];
fd_lookup[pollfds[count_pollfds].fd] = m;
break;
case LWS_CALLBACK_CHANGE_MODE_POLL_FD:
pollfds[fd_lookup[pa->fd]].events = pa->events;
break;
default:
break;
}
return 0;
}
/* dumb_increment protocol */
/*
* one of these is auto-created for each connection and a pointer to the
* appropriate instance is passed to the callback in the user parameter
*
* for this example protocol we use it to individualize the count for each
* connection.
*/
struct per_session_data__notif_client {
int number;
char *session_id;
struct nc_session *session;
};
struct session_with_mutex *get_ncsession_from_sid(const char *session_id)
{
struct session_with_mutex *locked_session = NULL;
if (session_id == NULL) {
return (NULL);
}
for (locked_session = netconf_sessions_list;
locked_session && strcmp(nc_session_get_id(locked_session->session), session_id);
locked_session = locked_session->next);
return locked_session;
}
/* rpc parameter is freed after the function call */
static int send_recv_process(struct nc_session *session, const char* UNUSED(operation), nc_rpc* rpc)
{
nc_reply *reply = NULL;
char *data = NULL;
int ret = EXIT_SUCCESS;
/* send the request and get the reply */
switch (nc_session_send_recv(session, rpc, &reply)) {
case NC_MSG_UNKNOWN:
if (nc_session_get_status(session) != NC_SESSION_STATUS_WORKING) {
ERROR("notifications: receiving rpc-reply failed.");
//cmd_disconnect(NULL);
ret = EXIT_FAILURE;
break;
}
ERROR("notifications: Unknown error occurred.");
ret = EXIT_FAILURE;
break;
case NC_MSG_NONE:
/* error occurred, but processed by callback */
break;
case NC_MSG_REPLY:
switch (nc_reply_get_type(reply)) {
case NC_REPLY_OK:
break;
case NC_REPLY_DATA:
DEBUG("notifications: recv: %s.", data = nc_reply_get_data (reply));
free(data);
break;
case NC_REPLY_ERROR:
/* wtf, you shouldn't be here !?!? */
DEBUG("notifications: operation failed, but rpc-error was not processed.");
ret = EXIT_FAILURE;
break;
default:
DEBUG("notifications: unexpected operation result.");
ret = EXIT_FAILURE;
break;
}
break;
default:
DEBUG("notifications: Unknown error occurred.");
ret = EXIT_FAILURE;
break;
}
nc_rpc_free(rpc);
nc_reply_free(reply);
return (ret);
}
/**
* \brief Callback to store incoming notification
* \param [in] eventtime - time when notification occured
* \param [in] content - content of notification
*/
static void notification_fileprint (time_t eventtime, const char* content)
{
struct session_with_mutex *target_session = NULL;
notification_t *ntf = NULL;
const char *session_id = NULL;
DEBUG("Accepted notif: %lu %s\n", (unsigned long int) eventtime, content);
session_id = pthread_getspecific(thread_key);
DEBUG("notification: fileprint getspecific (%s)", session_id);
if (pthread_rwlock_wrlock(&session_lock) != 0) {
ERROR("notifications: Error while locking rwlock");
return;
}
DEBUG("Get session with mutex from key %s.", session_id);
target_session = get_ncsession_from_sid(session_id);
if (target_session == NULL) {
ERROR("notifications: no session found last_session_key (%s)", session_id);
goto unlock_glob;
}
if (pthread_mutex_lock(&target_session->lock) != 0) {
ERROR("notifications: Error while locking rwlock");
}
DEBUG("notification: ready to push to notifications queue");
if (target_session->notif_count < NOTIFICATION_QUEUE_SIZE) {
++target_session->notif_count;
target_session->notifications = realloc(target_session->notifications,
target_session->notif_count * sizeof *target_session->notifications);
if (target_session->notifications) {
ntf = target_session->notifications + target_session->notif_count - 1;
}
}
if (ntf == NULL) {
ERROR("notifications: Failed to allocate element ");
goto unlock_all;
}
ntf->eventtime = eventtime;
ntf->content = strdup(content);
DEBUG("added notif to queue %u (%s)", (unsigned int) ntf->eventtime, "notification");
unlock_all:
if (pthread_mutex_unlock(&target_session->lock) != 0) {
ERROR("notifications: Error while unlocking rwlock");
}
unlock_glob:
if (pthread_rwlock_unlock(&session_lock) != 0) {
ERROR("notifications: Error while locking rwlock");
}
}
/**
* \brief Thread for libnetconf notifications dispatch
* \param [in] arg - struct ntf_thread_config * with nc_session
*/
void* notification_thread(void* arg)
{
struct ntf_thread_config *config = (struct ntf_thread_config*)arg;
DEBUG("notifications: in thread for libnetconf notifications");
/* store hash identification of netconf session for notifications printing callback */
if (pthread_setspecific(thread_key, config->session_id) != 0) {
ERROR("notifications: cannot set thread-specific hash value.");
}
DEBUG("notifications: dispatching");
ncntf_dispatch_receive(config->session, notification_fileprint);
DEBUG("notifications: ended thread for libnetconf notifications");
if (config->session_id != NULL) {
free(config->session_id);
}
if (config != NULL) {
free(config);
}
return (NULL);
}
int notif_subscribe(struct session_with_mutex *locked_session, const char *session_id, time_t start_time, time_t stop_time)
{
time_t start = -1;
time_t stop = -1;
struct nc_filter *filter = NULL;
char *stream = NULL;
nc_rpc *rpc = NULL;
pthread_t thread;
struct ntf_thread_config *tconfig;
struct nc_session *session;
DEBUG("notif_subscribe");
if (locked_session == NULL) {
DEBUG("notifications: no locked_session was given.");
/* Close notification client */
return -1;
}
pthread_mutex_lock(&locked_session->lock);
session = locked_session->session;
start = time(NULL) + start_time;
stop = time(NULL) + stop_time;
start = 0;
stop = 0;
DEBUG("notifications: history: %u %u", (unsigned int) start, (unsigned int) stop);
if (session == NULL) {
ERROR("notifications: NETCONF session not established.");
goto operation_failed;
}
/* check if notifications are allowed on this session */
if (nc_session_notif_allowed(session) == 0) {
ERROR("notifications: Notification subscription is not allowed on this session.");
goto operation_failed;
}
/* check times */
if (start != -1 && stop != -1 && start > stop) {
ERROR("notifications: Subscription start time must be lower than the end time.");
goto operation_failed;
}
DEBUG("Prepare to execute subscription.");
/* create requests */
rpc = nc_rpc_subscribe(stream, filter, (start_time == -1)?NULL:&start, (stop_time == 0)?NULL:&stop);
nc_filter_free(filter);
if (rpc == NULL) {
ERROR("notifications: creating an rpc request failed.");
goto operation_failed;
}
DEBUG("Send NC subscribe.");
create_err_reply_p();
if (send_recv_process(session, "subscribe", rpc) != 0) {
ERROR("Subscription RPC failed.");
goto operation_failed;
}
GETSPEC_ERR_REPLY
if (err_reply != NULL) {
free_err_reply();
ERROR("RPC-Error received and cleaned, because we can't send it anywhere.");
goto operation_failed;
}
rpc = NULL; /* just note that rpc is already freed by send_recv_process() */
locked_session->ntfc_subscribed = 1;
DEBUG("Create config for notification_thread.");
tconfig = malloc(sizeof(struct ntf_thread_config));
if (tconfig == NULL) {
ERROR("notifications: Allocation failed.");
goto operation_failed;
}
tconfig->session = session;
tconfig->session_id = strdup(session_id);
DEBUG("notifications: creating libnetconf notification thread (%s).", tconfig->session_id);
pthread_mutex_unlock(&locked_session->lock);
DEBUG("Create notification_thread.");
if (pthread_create(&thread, NULL, notification_thread, tconfig) != 0) {
ERROR("notifications: creating a thread for receiving notifications failed");
return -1;
}
pthread_detach(thread);
DEBUG("Subscription finished.");
return 0;
operation_failed:
pthread_mutex_unlock(&locked_session->lock);
return -1;
}
static int callback_notification(struct libwebsocket_context *context,
struct libwebsocket *wsi,
enum libwebsocket_callback_reasons reason,
void *user, void *in, size_t len)
{
int n = 0, m = 0, i;
unsigned char buf[LWS_SEND_BUFFER_PRE_PADDING + 40960 + LWS_SEND_BUFFER_POST_PADDING];
unsigned char *p = &buf[LWS_SEND_BUFFER_PRE_PADDING];
struct per_session_data__notif_client *pss = (struct per_session_data__notif_client *)user;
switch (reason) {
case LWS_CALLBACK_ESTABLISHED:
DEBUG("notification client connected.");
break;
case LWS_CALLBACK_SERVER_WRITEABLE:
if (pss->session_id == NULL) {
return 0;
}
//DEBUG("Callback server writeable.");
//DEBUG("lock session lock.");
if (pthread_rwlock_wrlock(&session_lock) != 0) {
DEBUG("Error while unlocking rwlock: %d (%s)", errno, strerror(errno));
return -1;
}
//DEBUG("get session_with_mutex for %s.", pss->session_id);
struct session_with_mutex *ls = get_ncsession_from_sid(pss->session_id);
if (ls == NULL) {
DEBUG("notification: session not found");
if (pthread_rwlock_unlock (&session_lock) != 0) {
DEBUG("Error while unlocking rwlock: %d (%s)", errno, strerror(errno));
return -1;
}
return -1;
}
pthread_mutex_lock(&ls->lock);
if (pthread_rwlock_unlock(&session_lock) != 0) {
DEBUG("Error while unlocking rwlock: %d (%s)", errno, strerror(errno));
}
//DEBUG("check for closed session.");
if (ls->closed == 1) {
DEBUG("unlock session key.");
if (pthread_rwlock_unlock (&session_lock) != 0) {
DEBUG("Error while unlocking unlock: %d (%s)", errno, strerror(errno));
return -1;
}
return -1;
}
//DEBUG("lock private lock.");
notification_t *notif = NULL;
if (ls->notif_count) {
DEBUG("notification: POP notifications for session");
for (i = ls->notif_count; i; --i) {
notif = ls->notifications + i - 1;
n = 0;
pthread_mutex_lock(&json_lock);
json_object *notif_json = json_object_new_object();
json_object_object_add(notif_json, "eventtime", json_object_new_int64(notif->eventtime));
json_object_object_add(notif_json, "content", json_object_new_string(notif->content));
pthread_mutex_unlock(&json_lock);
const char *msgtext = json_object_to_json_string(notif_json);
//n = sprintf((char *)p, "{\"eventtime\": \"%s\", \"content\": \"notification\"}", t);
n = sprintf((char *)p, "%s", msgtext);
DEBUG("ws send %dB in %lu", n, sizeof(buf));
m = libwebsocket_write(wsi, p, n, LWS_WRITE_TEXT);
if (lws_send_pipe_choked(wsi)) {
libwebsocket_callback_on_writable(context, wsi);
break;
}
pthread_mutex_lock(&json_lock);
json_object_put(notif_json);
pthread_mutex_unlock(&json_lock);
free(notif->content);
}
ls->notif_count = 0;
free(ls->notifications);
ls->notifications = NULL;
DEBUG("notification: POP notifications done");
}
//DEBUG("unlock private lock");
if (pthread_mutex_unlock(&ls->lock) != 0) {
DEBUG("notification: cannot unlock session");
}
//DEBUG("unlock session lock");
if (m < n) {
DEBUG("ERROR %d writing to di socket.", n);
return -1;
}
break;
case LWS_CALLBACK_RECEIVE:
DEBUG("Callback receive.");
DEBUG("received: (%s)", (char *)in);
if (pss->session_id == NULL) {
char *sid_end;
int start = -1;
time_t stop = time(NULL) + 30;
sid_end = strchr(in, ' ');
pss->session_id = strndup(in, sid_end - (char *)in);
++sid_end;
sscanf(sid_end, "%d %d", (int *) &start, (int *) &stop);
DEBUG("notification: SID (%s) from (%s) (%i,%i)", pss->session_id, (char *) in, (int) start, (int) stop);
DEBUG("lock session lock");
if (pthread_rwlock_rdlock (&session_lock) != 0) {
DEBUG("Error while locking rwlock: %d (%s)", errno, strerror(errno));
return -1;
}
DEBUG("get session with ID (%s)", pss->session_id);
struct session_with_mutex *ls = get_ncsession_from_sid(pss->session_id);
if (ls == NULL) {
DEBUG("notification: session_id not found (%s)", pss->session_id);
DEBUG("unlock session lock");
if (pthread_rwlock_unlock (&session_lock) != 0) {
DEBUG("Error while unlocking rwlock: %d (%s)", errno, strerror(errno));
}
DEBUG("Close notification client");
return -1;
}
DEBUG("lock private lock");
pthread_mutex_lock(&ls->lock);
DEBUG("unlock session lock");
if (pthread_rwlock_unlock (&session_lock) != 0) {
DEBUG("Error while unlocking rwlock: %d (%s)", errno, strerror(errno));
}
DEBUG("Found session to subscribe notif.");
if (ls->closed == 1) {
DEBUG("session already closed - handle no notification");
DEBUG("unlock private lock");
pthread_mutex_unlock(&ls->lock);
DEBUG("Close notification client");
return -1;
}
if (ls->ntfc_subscribed != 0) {
DEBUG("notification: already subscribed");
DEBUG("unlock private lock");
pthread_mutex_unlock(&ls->lock);
/* do not close client, only do not subscribe again */
return 0;
}
DEBUG("notification: prepare to subscribe stream");
DEBUG("unlock session lock");
pthread_mutex_unlock(&ls->lock);
/* notif_subscribe locks on its own */
return notif_subscribe(ls, pss->session_id, (time_t) start, (time_t) stop);
}
if (len < 6)
break;
break;
/*
* this just demonstrates how to use the protocol filter. If you won't
* study and reject connections based on header content, you don't need
* to handle this callback
*/
case LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION:
//dump_handshake_info(wsi);
/* you could return non-zero here and kill the connection */
break;
//gives segfault :-(
//case LWS_CALLBACK_CLOSED:
// if (pss->session_key != NULL) {
// free(pss->session_key);
// }
// if (pss != NULL) {
// free(pss);
// }
default:
break;
}
return 0;
}
/* list of supported protocols and callbacks */
static struct libwebsocket_protocols protocols[] = {
/* first protocol must always be HTTP handler */
{
"http-only", /* name */
callback_http, /* callback */
sizeof (struct per_session_data__http), /* per_session_data_size */
0, /* max frame size / rx buffer */
0,
NULL,
NULL,
0
},
{
"notification-protocol",
callback_notification,
sizeof(struct per_session_data__notif_client),
4000,
0,
NULL,
NULL,
0
},
{ NULL, NULL, 0, 0, 0, NULL, NULL, 0 } /* terminator */
};
/**
* initialization of notification module
*/
int notification_init(void)
{
//char cert_path[1024];
//char key_path[1024];
//int use_ssl = 0;
struct lws_context_creation_info info;
int opts = 0;
//char interface_name[128] = "";
const char *iface = NULL;
int debug_level = 7;
memset(&info, 0, sizeof info);
info.port = NOTIFICATION_SERVER_PORT;
/* tell the library what debug level to emit and to send it to syslog */
lws_set_log_level(debug_level, lwsl_emit_syslog);
DEBUG("Initialization of libwebsocket");
//lwsl_notice("libwebsockets test server - "
// "(C) Copyright 2010-2013 Andy Green <andy@warmcat.com> - "
// "licensed under LGPL2.1\n");
max_poll_elements = getdtablesize();
pollfds = malloc(max_poll_elements * sizeof (struct pollfd));
fd_lookup = malloc(max_poll_elements * sizeof (int));
if (pollfds == NULL || fd_lookup == NULL) {
ERROR("notifications: Out of memory pollfds=%d\n", max_poll_elements);
return -1;
}
info.iface = iface;
info.protocols = protocols;
//snprintf(cert_path, sizeof(cert_path), "%s/libwebsockets-test-server.pem", resource_path);
//snprintf(key_path, sizeof(cert_path), "%s/libwebsockets-test-server.key.pem", resource_path);
//info.ssl_cert_filepath = cert_path;
//info.ssl_private_key_filepath = key_path;
info.gid = -1;
info.uid = -1;
info.options = opts;
/* create server */
context = libwebsocket_create_context(&info);
if (context == NULL) {
DEBUG("libwebsocket init failed.");
return -1;
}
if (pthread_key_create(&thread_key, NULL) != 0) {
ERROR("notifications: pthread_key_create failed");
}
return 0;
}
void notification_close(void)
{
if (context) {
libwebsocket_context_destroy(context);
}
free(pollfds);
free(fd_lookup);
DEBUG("libwebsockets-test-server exited cleanly\n");
}
/**
* \brief send notification if any
* \return < 0 on error
*/
int notification_handle()
{
static struct timeval tv;
static unsigned int olds = 0;
int n = 0;
gettimeofday(&tv, NULL);
/*
* This provokes the LWS_CALLBACK_SERVER_WRITEABLE for every
* live websocket connection using the DUMB_INCREMENT protocol,
* as soon as it can take more packets (usually immediately)
*/
if (((unsigned int)tv.tv_sec - olds) > 0) {
libwebsocket_callback_on_writable_all_protocol(&protocols[PROTOCOL_NOTIFICATION]);
olds = tv.tv_sec;
}
/*
* this represents an existing server's single poll action
* which also includes libwebsocket sockets
*/
n = poll(pollfds, count_pollfds, 50);
if (n < 0)
return n;
if (n) {
for (n = 0; n < count_pollfds; n++) {
if (pollfds[n].revents) {
/*
* returns immediately if the fd does not
* match anything under libwebsockets
* control
*/
if (libwebsocket_service_fd(context, &pollfds[n]) < 0) {
return 1;
}
}
}
}
return 0;
}
#endif
#ifndef WITH_NOTIFICATIONS
#ifdef TEST_NOTIFICATION_SERVER
int main(int argc, char **argv)
{
if (notification_init(NULL, NULL) == -1) {
fprintf(stderr, "Error during initialization\n");
return 1;
}
while (!force_exit) {
notification_handle();
}
notification_close();
}
#endif
#endif