[ovs-dev] [RFC PATCH] utilities: Add OpenFlow proxy ovs-ofproxy

Xiao Liang shaw.leon at gmail.com
Fri Mar 9 02:54:01 UTC 2018


Add ovs-ofproxy to enable tools like ovs-ofctl to work with non-OVS
switches which don't support controller-initiated connection.
The proxy listens for switches connection, opens a unix socket on behalf of
each switch.

Example:
    Start proxy
        $ ovs-ofproxy -O OpenFlow13 ptcp:6653
    After switch is connected, use ovs-ofctl:
        $ ovs-ofctl -O Openflow13 show unix:/var/run/openvswitch/tcp:...
    (see log of proxy for the socket path)

Signed-off-by: Xiao Liang <shaw.leon at gmail.com>
---
 utilities/.gitignore    |   1 +
 utilities/automake.mk   |   8 +-
 utilities/ovs-ofproxy.c | 680 ++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 688 insertions(+), 1 deletion(-)
 create mode 100644 utilities/ovs-ofproxy.c

diff --git a/utilities/.gitignore b/utilities/.gitignore
index 34c58f20f..bc702f49a 100644
--- a/utilities/.gitignore
+++ b/utilities/.gitignore
@@ -18,6 +18,7 @@
 /ovs-lib
 /ovs-ofctl
 /ovs-ofctl.8
+/ovs-ofproxy
 /ovs-parse-backtrace
 /ovs-pcap
 /ovs-pcap.1
diff --git a/utilities/automake.mk b/utilities/automake.mk
index 1636cb93e..a58681b50 100644
--- a/utilities/automake.mk
+++ b/utilities/automake.mk
@@ -3,7 +3,8 @@ bin_PROGRAMS += \
 	utilities/ovs-testcontroller \
 	utilities/ovs-dpctl \
 	utilities/ovs-ofctl \
-	utilities/ovs-vsctl
+	utilities/ovs-vsctl \
+	utilities/ovs-ofproxy
 bin_SCRIPTS += utilities/ovs-docker \
 	utilities/ovs-pki
 if HAVE_PYTHON
@@ -134,6 +135,11 @@ utilities_ovs_ofctl_LDADD = \
 utilities_ovs_vsctl_SOURCES = utilities/ovs-vsctl.c
 utilities_ovs_vsctl_LDADD = lib/libopenvswitch.la
 
+utilities_ovs_ofproxy_SOURCES = utilities/ovs-ofproxy.c
+utilities_ovs_ofproxy_LDADD = \
+	ofproto/libofproto.la \
+	lib/libopenvswitch.la
+
 if LINUX
 sbin_PROGRAMS += utilities/ovs-vlan-bug-workaround
 utilities_ovs_vlan_bug_workaround_SOURCES = utilities/ovs-vlan-bug-workaround.c
diff --git a/utilities/ovs-ofproxy.c b/utilities/ovs-ofproxy.c
new file mode 100644
index 000000000..fde450936
--- /dev/null
+++ b/utilities/ovs-ofproxy.c
@@ -0,0 +1,680 @@
+#include <config.h>
+#include <errno.h>
+#include <getopt.h>
+#include <unistd.h>
+
+#include "openflow/openflow.h"
+
+#include "openvswitch/hmap.h"
+#include "openvswitch/list.h"
+#include "openvswitch/ofp-msgs.h"
+#include "openvswitch/ofp-util.h"
+#include "openvswitch/ofpbuf.h"
+#include "openvswitch/poll-loop.h"
+#include "openvswitch/rconn.h"
+#include "openvswitch/vconn.h"
+#include "openvswitch/vlog.h"
+
+#include "lib/command-line.h"
+#include "lib/daemon.h"
+#include "lib/dirs.h"
+#include "lib/fatal-signal.h"
+#include "lib/hash.h"
+#include "lib/ofp-version-opt.h"
+#include "lib/socket-util.h"
+#include "lib/stream-ssl.h"
+#include "lib/timeval.h"
+#include "lib/unixctl.h"
+#include "lib/util.h"
+
+VLOG_DEFINE_THIS_MODULE(ofproxy);
+
+/* XID management
+ *
+ * Maintain an hmap and an ordered list of outstanding XID entries for each
+ * switch, and a list for controllers.
+ *
+ * On message received from controller:
+ *  - Allocate a new XID and entry for the message.
+ *  - Insert the XID entry to hmap and lists.
+ *  - Set new XID and send to switch.
+ *
+ * On message received from switch:
+ *  - Lookup XID entry in hmap.
+ *  - If type is BARRIER_REPLY, remove entries prior to the corresponding
+ *    BARRIER_REQUEST.
+ *  - If the entry has controller information, set original XID and send to
+ *    controller.
+ *
+ * Periodically send BARRIER_REQUEST if there're XID entries.
+ */
+struct xid_entry {
+    struct hmap_node map_node;      /* Links to xid_map of switch. */
+    uint32_t xid;
+
+    struct switch_context *sw;
+    struct ovs_list sw_node;        /* Links to xid_list of switch. */
+
+    struct ctlr_context *ctlr;      /* Originating controller, if any */
+    uint32_t orig_xid;
+    struct ovs_list ctlr_node;      /* Links to xid_list of controller. */
+};
+
+enum switch_state {
+    S_CONNECTING,
+    S_ESTABLISHED,
+    S_ERROR
+};
+
+struct ctlr_context {
+    struct ovs_list list_node;      /* List to ctlrs of switch. */
+    struct switch_context *sw;
+    struct rconn *rconn;
+    struct ovs_list xid_list;       /* Outstanding XIDs. */
+};
+
+struct switch_context {
+    struct ovs_list list_node;
+    struct rconn *rconn;
+    const char *name;
+    enum switch_state state;
+    uint32_t protocol_version;
+
+    struct pvconn *pvconn;
+    struct ovs_list ctlrs;          /* List of active controllers. */
+
+    uint32_t next_xid;
+    struct ovs_list xid_list;       /* Outstanding XID entries, ordered. */
+    struct hmap xid_map;            /* XID entries indexed by XID */
+
+    long long barrier_timer;
+
+    struct pvconn *snoop;
+};
+
+static struct switch_context *switch_create(struct vconn *vconn);
+static void switch_destroy(struct switch_context *sw);
+static void switch_run(struct switch_context *sw);
+static void switch_wait(struct switch_context *sw);
+static int switch_ctlr_pvconn_open(struct switch_context *sw);
+static int switch_snoop_pvconn_open(struct switch_context *sw);
+static void switch_send_barrier(struct switch_context *sw);
+static void process_sw_message(struct switch_context *sw, struct ofpbuf *msg);
+static void switch_xid_clear__(struct switch_context *sw);
+
+static struct ctlr_context *ctlr_create(struct vconn *vconn,
+                                        struct switch_context *sw);
+static void ctlr_destroy(struct ctlr_context *ctlr);
+static void ctlr_destroy__(struct ctlr_context *ctlr);
+static void ctlr_xid_clear(struct ctlr_context *ctlr);
+static void ctlr_run(struct ctlr_context *ctlr);
+static void ctlr_wait(struct ctlr_context *ctlr);
+static void process_ctlr_message(struct ctlr_context *ctlr,
+                                 struct ofpbuf *msg);
+
+static void xid_insert(struct xid_entry *entry);
+static void xid_remove(struct xid_entry *entry);
+static struct xid_entry *xid_find(struct hmap *xid_map,
+                                  uint32_t xid);
+static void xid_barrier(struct xid_entry *entry);
+
+static void ofproxy_exit(struct unixctl_conn *conn, int argc,
+                         const char *argv[], void *exiting);
+
+/* Options. */
+static char *unixctl_path = NULL;
+static char *pvconn_name = "ptcp:";
+static uint32_t version_mask = 0;
+static long long barrier_interval = 5;
+static bool snoop = false;
+
+static void
+xid_insert(struct xid_entry *entry)
+{
+    ovs_list_push_back(&entry->sw->xid_list, &entry->sw_node);
+    hmap_insert(&entry->sw->xid_map, &entry->map_node,
+                hash_int(entry->xid, 0));
+    if (entry->ctlr) {
+        ovs_list_push_back(&entry->ctlr->xid_list, &entry->ctlr_node);
+    }
+}
+
+static void
+xid_remove(struct xid_entry *entry)
+{
+    ovs_list_remove(&entry->sw_node);
+    hmap_remove(&entry->sw->xid_map, &entry->map_node);
+    if (entry->ctlr) {
+        ovs_list_remove(&entry->ctlr_node);
+    }
+}
+
+static struct xid_entry *
+xid_find(struct hmap *xid_map, uint32_t xid)
+{
+    struct xid_entry *entry;
+    HMAP_FOR_EACH_WITH_HASH (entry, map_node,
+                             hash_int(xid, 0), xid_map) {
+        if (entry->xid == xid) {
+            return entry;
+        }
+    }
+    return NULL;
+}
+
+/* Flushes all prior XIDs. Called when BARRIER_REPLY is received. */
+static void
+xid_barrier(struct xid_entry *entry)
+{
+    struct xid_entry *curr, *next;
+    LIST_FOR_EACH_SAFE (curr, next, sw_node, &entry->sw->xid_list) {
+        if (curr == entry) {
+            break;
+        } else {
+            xid_remove(curr);
+            free(curr);
+        }
+    }
+}
+
+static struct switch_context *
+switch_create(struct vconn *vconn)
+{
+    struct switch_context *sw = xzalloc(sizeof(struct switch_context));
+
+    sw->rconn = rconn_create(0, 0, DSCP_DEFAULT, version_mask);
+    rconn_connect_unreliably(sw->rconn, vconn, NULL);
+    sw->name = rconn_get_name(sw->rconn);
+
+    ovs_list_init(&sw->ctlrs);
+    ovs_list_init(&sw->xid_list);
+    hmap_init(&sw->xid_map);
+
+    if (barrier_interval > 0) {
+        sw->barrier_timer = time_msec() + barrier_interval;
+    }
+
+    return sw;
+}
+
+/* Clears all XIDs. Called only when switch is being destroyed. */
+static void
+switch_xid_clear__(struct switch_context *sw)
+{
+    struct xid_entry *entry;
+    HMAP_FOR_EACH_POP (entry, map_node, &sw->xid_map) {
+        free(entry);
+    }
+    hmap_destroy(&sw->xid_map);
+}
+
+static void
+switch_destroy(struct switch_context *sw)
+{
+    if (sw->pvconn) {
+        pvconn_close(sw->pvconn);
+    }
+    rconn_destroy(sw->rconn);
+
+    struct ctlr_context *ctlr;
+    LIST_FOR_EACH_POP (ctlr, list_node, &sw->ctlrs) {
+        ctlr_destroy__(ctlr);
+    }
+
+    switch_xid_clear__(sw);
+
+    free(sw);
+}
+
+static void
+switch_run(struct switch_context *sw)
+{
+    rconn_run(sw->rconn);
+
+    /* TODO: Postpone pvconn_open after FEATURES_REPLY. */
+    if (sw->state == S_CONNECTING) {
+        int version;
+        if ((version = rconn_get_version(sw->rconn)) != -1) {
+            sw->protocol_version = version;
+            VLOG_INFO("switch %s negotiated version: %s",
+                      sw->name, ofputil_version_to_string(version));
+            if (switch_ctlr_pvconn_open(sw) == 0) {
+                sw->state = S_ESTABLISHED;
+            } else {
+                sw->state = S_ERROR;
+            }
+            if (snoop) {
+                switch_snoop_pvconn_open(sw);
+            }
+        }
+    }
+
+    while (sw->pvconn) {
+        struct vconn *new_vconn;
+        int error = pvconn_accept(sw->pvconn, &new_vconn);
+        if (!error) {
+            ctlr_create(new_vconn, sw);
+        } else if (error == EAGAIN) {
+            break;
+        } else {
+            VLOG_WARN("pvconn error, switch: %s", sw->name);
+            pvconn_close(sw->pvconn);
+            sw->pvconn = NULL;
+        }
+    }
+
+    while (sw->snoop) {
+        struct vconn *new_vconn;
+        int error = pvconn_accept(sw->snoop, &new_vconn);
+        if (!error) {
+            rconn_add_monitor(sw->rconn, new_vconn);
+        } else if (error == EAGAIN) {
+            break;
+        } else {
+            VLOG_WARN("pvconn error, switch: %s", sw->name);
+            pvconn_close(sw->pvconn);
+            sw->pvconn = NULL;
+        }
+    }
+
+    struct ctlr_context *ctlr, *next;
+    LIST_FOR_EACH_SAFE (ctlr, next, list_node, &sw->ctlrs) {
+        ctlr_run(ctlr);
+        if (!rconn_is_alive(ctlr->rconn)) {
+            ctlr_destroy(ctlr);
+        }
+    }
+
+    for (int i = 0; i < 50; i++) {
+        struct ofpbuf *msg;
+        msg = rconn_recv(sw->rconn);
+        if (!msg) {
+            break;
+        }
+        process_sw_message(sw, msg);
+        ofpbuf_delete(msg);
+    }
+
+    if (barrier_interval > 0 &&
+        time_msec() >= sw->barrier_timer) {
+        if (!ovs_list_is_empty(&sw->xid_list)) {
+            switch_send_barrier(sw);
+        }
+        sw->barrier_timer = time_msec() + barrier_interval;
+    }
+}
+
+static void
+switch_wait(struct switch_context *sw)
+{
+    rconn_run_wait(sw->rconn);
+    rconn_recv_wait(sw->rconn);
+    if (sw->pvconn) {
+        pvconn_wait(sw->pvconn);
+    }
+
+    struct ctlr_context *ctlr;
+    LIST_FOR_EACH (ctlr, list_node, &sw->ctlrs) {
+        ctlr_wait(ctlr);
+    }
+
+    if (barrier_interval > 0) {
+        poll_timer_wait_until(sw->barrier_timer);
+    }
+}
+
+static char *
+switch_ctlr_pvconn_name(struct switch_context *sw)
+{
+    /* TODO: support user-provided template, like using datapath_id, etc. */
+    return xasprintf("punix:%s/%s.proxy", ovs_rundir(), sw->name);
+}
+
+static int
+switch_ctlr_pvconn_open(struct switch_context *sw)
+{
+    char *name = switch_ctlr_pvconn_name(sw);
+    int error = pvconn_open(name, 1 << sw->protocol_version,
+                            DSCP_DEFAULT, &sw->pvconn);
+    if (error) {
+        VLOG_WARN("failed to listen for controller on: %s", name);
+    } else {
+        VLOG_INFO("listening for controller on: %s", name);
+    }
+    free(name);
+    return error;
+}
+
+static char *
+switch_snoop_pvconn_name(struct switch_context *sw)
+{
+    return xasprintf("punix:%s/%s.snoop", ovs_rundir(), sw->name);
+}
+
+static int
+switch_snoop_pvconn_open(struct switch_context *sw)
+{
+    char *name = switch_snoop_pvconn_name(sw);
+    int error = pvconn_open(name, 0,
+                            DSCP_DEFAULT, &sw->snoop);
+    if (error) {
+        VLOG_WARN("failed to listen for snooping on: %s", name);
+    } else {
+        VLOG_INFO("listening for snooping on: %s", name);
+    }
+    free(name);
+    return error;
+}
+
+static struct ctlr_context *
+ctlr_create(struct vconn *vconn, struct switch_context *sw)
+{
+    struct ctlr_context *ctlr = xzalloc(sizeof(struct ctlr_context));
+
+    ctlr->sw = sw;
+    ctlr->rconn = rconn_create(0, 0, DSCP_DEFAULT, 0);
+    rconn_connect_unreliably(ctlr->rconn, vconn, NULL);
+    ovs_list_init(&ctlr->xid_list);
+
+    ovs_list_push_back(&sw->ctlrs, &ctlr->list_node);
+
+    return ctlr;
+}
+
+static void
+ctlr_xid_clear(struct ctlr_context *ctlr)
+{
+    struct xid_entry *entry, *next;
+    LIST_FOR_EACH_SAFE (entry, next, ctlr_node, &ctlr->xid_list) {
+        xid_remove(entry);
+        free(entry);
+    }
+}
+
+static void
+ctlr_destroy(struct ctlr_context *ctlr)
+{
+    ovs_list_remove(&ctlr->list_node);
+    ctlr_xid_clear(ctlr);
+    ctlr_destroy__(ctlr);
+}
+
+static void
+ctlr_destroy__(struct ctlr_context *ctlr)
+{
+    rconn_destroy(ctlr->rconn);
+    free(ctlr);
+}
+
+static void
+ctlr_run(struct ctlr_context *ctlr)
+{
+    rconn_run(ctlr->rconn);
+
+    for (int i = 0; i < 50; i++) {
+        struct ofpbuf *msg;
+        msg = rconn_recv(ctlr->rconn);
+        if (!msg) {
+            break;
+        }
+        process_ctlr_message(ctlr, msg);
+        ofpbuf_delete(msg);
+    }
+}
+
+static void
+ctlr_wait(struct ctlr_context *ctlr)
+{
+    rconn_run_wait(ctlr->rconn);
+    rconn_recv_wait(ctlr->rconn);
+}
+
+/* Clone ofp message and alter xid */
+static struct ofpbuf *
+ofp_msg_dup_xid(struct ofpbuf *msg, uint32_t xid)
+{
+    struct ofpbuf *new_msg = ofpbuf_clone(msg);
+    ((struct ofp_header *)new_msg->data)->xid = htonl(xid);
+    return new_msg;
+}
+
+static void
+process_ctlr_message(struct ctlr_context *ctlr, struct ofpbuf *msg)
+{
+    enum ofptype type;
+    const struct ofp_header *header = msg->data;
+    if (ofptype_decode(&type, header) == 0) {
+        /* TODO: handle controller state change (role, set_async, etc.) */
+        if (type == OFPTYPE_ECHO_REQUEST) {
+            rconn_send(ctlr->rconn, ofputil_encode_echo_reply(header), NULL);
+        } else {
+            struct xid_entry *entry =
+                xzalloc(sizeof(struct xid_entry));
+            entry->xid = ctlr->sw->next_xid++;
+            entry->orig_xid = ntohl(header->xid);
+            entry->sw = ctlr->sw;
+            entry->ctlr = ctlr;
+            xid_insert(entry);
+            rconn_send(ctlr->sw->rconn,
+                       ofp_msg_dup_xid(msg, entry->xid),
+                       NULL);
+        }
+    }
+}
+
+static void
+process_sw_message(struct switch_context *sw, struct ofpbuf *msg)
+{
+    enum ofptype type;
+    const struct ofp_header *header = msg->data;
+    if (ofptype_decode(&type, header) == 0) {
+        /* TODO: handle aync messages. */
+        if (type == OFPTYPE_ECHO_REQUEST) {
+            rconn_send(sw->rconn, ofputil_encode_echo_reply(header), NULL);
+        } else {
+            uint32_t xid = ntohl(header->xid);
+            struct xid_entry *entry = xid_find(&sw->xid_map, xid);
+            if (entry) {
+                if (type == OFPTYPE_BARRIER_REPLY) {
+                    xid_barrier(entry);
+                }
+                if (entry->ctlr) {
+                    rconn_send(entry->ctlr->rconn,
+                               ofp_msg_dup_xid(msg, entry->orig_xid),
+                               NULL);
+                } else {
+                    xid_remove(entry);
+                }
+            }
+        }
+    }
+}
+
+static void
+switch_send_barrier(struct switch_context *sw)
+{
+    struct xid_entry *entry = xzalloc(sizeof(struct xid_entry));
+    entry->xid = sw->next_xid++;
+    entry->sw = sw;
+    xid_insert(entry);
+    struct ofpbuf *msg = ofputil_encode_barrier_request(sw->protocol_version);
+    ((struct ofp_header *)msg->data)->xid = htonl(entry->xid);
+    rconn_send(sw->rconn, msg, NULL);
+}
+
+static void
+ofproxy_exit(struct unixctl_conn *conn, int argc OVS_UNUSED,
+             const char *argv[] OVS_UNUSED, void *exiting)
+{
+    *(bool *)exiting = true;
+    unixctl_command_reply(conn, NULL);
+}
+
+static void
+parse_options(int argc, char *argv[])
+{
+    enum {
+        OPT_UNIXCTL = UCHAR_MAX + 1,
+        OPT_BARRIER_INTERVAL,
+        OPT_SNOOP,
+        DAEMON_OPTION_ENUMS,
+        OFP_VERSION_OPTION_ENUMS,
+        VLOG_OPTION_ENUMS,
+        SSL_OPTION_ENUMS,
+    };
+
+    static const struct option long_options[] = {
+        {"unixctl", required_argument, NULL,  OPT_UNIXCTL},
+        {"barrier-interval", required_argument, NULL, OPT_BARRIER_INTERVAL},
+        {"enable-snoop", no_argument, NULL, OPT_SNOOP},
+        DAEMON_LONG_OPTIONS,
+        OFP_VERSION_LONG_OPTIONS,
+        VLOG_LONG_OPTIONS,
+        STREAM_SSL_LONG_OPTIONS,
+        {NULL, 0, NULL, 0},
+    };
+    char *short_options = ovs_cmdl_long_options_to_short_options(long_options);
+
+    for (;;) {
+        int c;
+
+        c = getopt_long(argc, argv, short_options, long_options, NULL);
+        if (c == -1) {
+            break;
+        }
+
+        switch (c) {
+            case OPT_UNIXCTL:
+                unixctl_path = optarg;
+                break;
+
+            case OPT_BARRIER_INTERVAL:
+                barrier_interval = (long long)atoi(optarg) * 1000;
+                break;
+
+            case OPT_SNOOP:
+                snoop = true;
+                break;
+
+                DAEMON_OPTION_HANDLERS
+                OFP_VERSION_OPTION_HANDLERS
+                VLOG_OPTION_HANDLERS
+                STREAM_SSL_OPTION_HANDLERS
+
+            case '?':
+                exit(EXIT_FAILURE);
+
+            case 0:
+                break;
+
+            default:
+                ovs_abort(0, "unknow option");
+        }
+    }
+
+    free(short_options);
+
+    version_mask = get_allowed_ofp_versions();
+
+    if (argc > optind) {
+        pvconn_name = argv[optind];
+    }
+}
+
+
+
+int
+main(int argc, char *argv[])
+{
+    int error;
+    bool exiting = false;
+
+    struct unixctl_server *server = NULL;
+    struct ovs_list switches = OVS_LIST_INITIALIZER(&switches);
+
+    set_program_name(argv[0]);
+    ovs_cmdl_proctitle_init(argc, argv);
+    service_start(&argc, &argv);
+    parse_options(argc, argv);
+    fatal_ignore_sigpipe();
+
+    daemon_become_new_user(false);
+
+    struct pvconn *pvconn;
+    error = pvconn_open(pvconn_name, version_mask, DSCP_DEFAULT, &pvconn);
+    if (error) {
+        ovs_fatal(0, "failed to listen");
+    }
+
+    daemonize_start(false);
+    if (unixctl_path) {
+        error = unixctl_server_create(unixctl_path, &server);
+        if (error) {
+            ovs_fatal(error, "failed to create unixctl server");
+        }
+        unixctl_command_register("exit", "", 0, 0, ofproxy_exit, &exiting);
+    }
+    daemonize_complete();
+
+    while (pvconn || !ovs_list_is_empty(&switches)) {
+        while (pvconn) {
+            struct vconn *new_vconn;
+            error = pvconn_accept(pvconn, &new_vconn);
+            if (!error) {
+                struct switch_context *sw = switch_create(new_vconn);
+                ovs_list_insert(&switches, &sw->list_node);
+                VLOG_INFO("switch %s connected", sw->name);
+            } else if (error == EAGAIN) {
+                break;
+            } else {
+                VLOG_WARN("pvconn error");
+                pvconn_close(pvconn);
+                pvconn = NULL;
+            }
+        }
+
+        struct switch_context *sw, *next;
+        LIST_FOR_EACH_SAFE (sw, next, list_node, &switches) {
+            switch_run(sw);
+            if (!rconn_is_alive(sw->rconn)) {
+                VLOG_INFO("switch %s disconnected", sw->name);
+                ovs_list_remove(&sw->list_node);
+                switch_destroy(sw);
+            }
+        }
+
+        if (server) {
+            unixctl_server_run(server);
+        }
+        if (exiting) {
+            break;
+        }
+
+        if (pvconn) {
+            pvconn_wait(pvconn);
+        }
+        LIST_FOR_EACH (sw, list_node, &switches) {
+            switch_wait(sw);
+        }
+        if (server) {
+            unixctl_server_wait(server);
+        }
+
+        poll_block();
+    }
+
+    if (pvconn) {
+        pvconn_close(pvconn);
+    }
+    struct switch_context *sw;
+    LIST_FOR_EACH_POP (sw, list_node, &switches) {
+        switch_destroy(sw);
+    }
+    if (server) {
+        unixctl_server_destroy(server);
+    }
+    service_stop();
+
+    return 0;
+}
+
-- 
2.16.2



More information about the dev mailing list