From cbce382e9cc06bb284f4d9f632e903bca6de0c67 Mon Sep 17 00:00:00 2001 From: Christian Pointner Date: Sat, 2 May 2015 21:29:38 +0200 Subject: moved to single directory diff --git a/.gitignore b/.gitignore index 6c773ce..dbc5999 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ -rhnop-client/include.mk -rhnop-server/nopsyncd -rhnop-server/include.mk -rhnop-server/config.h -rhnop-server/*.o -rhnop-server/*.d -rhnop-server/*.d.* +src/include.mk +src/nopsyncd +src/config.h +src/*.o +src/*.d +src/*.d.* diff --git a/src/Makefile b/src/Makefile new file mode 100644 index 0000000..e9af20a --- /dev/null +++ b/src/Makefile @@ -0,0 +1,122 @@ +## +## rhnop +## +## Copyright (C) 2011-2015 Christian Pointner +## +## This file is part of rhnop. +## +## rhnop is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation, either version 3 of the License, or +## any later version. +## +## rhnop 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 General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with rhnop. If not, see . +## + +ifneq ($(MAKECMDGOALS),distclean) +include include.mk +endif + +EXECUTABLE := nopsyncd + +C_OBJS := l_pipe.o \ + nopsyncd.o + +C_SRCS := $(C_OBJS:%.o=%.c) + +.PHONY: clean distclean install install-bin install-etc uninstall remove purge + +all: $(EXECUTABLE) + +%.d: %.c + @set -e; rm -f $@; \ + $(CC) -MM $(CFLAGS) $< > $@.$$$$; \ + sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \ + rm -f $@.$$$$; echo '(re)building $@' + +ifneq ($(MAKECMDGOALS),distclean) +-include $(C_SRCS:%.c=%.d) +endif + +$(EXECUTABLE): $(C_OBJS) + $(CC) $(C_OBJS) -o $@ $(LDFLAGS) + +%.o: %.c + $(CC) $(CFLAGS) -c $< + +strip: $(EXECUTABLE) + $(STRIP) -s $(EXECUTABLE) + + +distclean: clean + find . -name *.o -exec rm -f {} \; + find . -name "*.\~*" -exec rm -rf {} \; + rm -f include.mk + rm -f config.h + +clean: + rm -f *.o + rm -f *.d + rm -f *.d.* + rm -f $(EXECUTABLE) + +INSTALL_TARGETS := install-bin install-lib install-etc +REMOVE_TARGETS := remove-bin remove-lib remove-etc + +install: all $(INSTALL_TARGETS) + +install-bin: $(EXECUTABLE) + $(INSTALL) -d $(DESTDIR)$(BINDIR) + $(INSTALL) -m 755 $(EXECUTABLE) $(DESTDIR)$(BINDIR) + $(INSTALL) -m 755 noprml $(DESTDIR)$(BINDIR) + $(INSTALL) -m 755 nopcollectd $(DESTDIR)$(BINDIR) + $(SED) -e 's#^conffile = "nopcollectd#conffile = "$(ETCDIR)/rhnop/nopcollectd#' -i $(DESTDIR)$(BINDIR)/nopcollectd + $(SED) -e 's#_rhnoplibdir_/?.lua#$(LIBDIR)/?.lua#' -i $(DESTDIR)$(BINDIR)/nopcollectd + $(INSTALL) -m 755 nopsysstated $(DESTDIR)$(BINDIR) + $(SED) -e 's#^conffile = "nopsysstated#conffile = "$(ETCDIR)/rhnop/nopsysstated#' -i $(DESTDIR)$(BINDIR)/nopsysstated + $(SED) -e 's#_rhnoplibdir_/?.lua#$(LIBDIR)/?.lua#' -i $(DESTDIR)$(BINDIR)/nopsysstated + +install-lib: + $(INSTALL) -d $(DESTDIR)$(LIBDIR) + $(INSTALL) -m 644 conf.lua $(DESTDIR)$(LIBDIR) + $(INSTALL) -m 644 db.lua $(DESTDIR)$(LIBDIR) + $(INSTALL) -m 644 playlog.lua $(DESTDIR)$(LIBDIR) + $(INSTALL) -m 644 rddb.lua $(DESTDIR)$(LIBDIR) + $(INSTALL) -m 644 qlistener.lua $(DESTDIR)$(LIBDIR) + $(SED) -e 's#^conffile = "nopsyncd#conffile = "$(ETCDIR)/rhnop/nopsyncd#' -i $(DESTDIR)$(LIBDIR)/qlistener.lua + $(INSTALL) -m 644 tcpserver.lua $(DESTDIR)$(LIBDIR) + +install-etc: + $(INSTALL) -d $(DESTDIR)$(ETCDIR)/rhnop + $(INSTALL) -m 640 nopsyncd.conf $(DESTDIR)$(ETCDIR)/rhnop + $(INSTALL) -m 640 nopcollectd.*.conf $(DESTDIR)$(ETCDIR)/rhnop + $(INSTALL) -m 640 nopsysstated.conf $(DESTDIR)$(ETCDIR)/rhnop + +uninstall: remove + +remove: $(REMOVE_TARGETS) + +remove-bin: + rm -f $(DESTDIR)$(BINDIR)/$(EXECUTABLE) + rm -f $(DESTDIR)$(BINDIR)/noprml + rm -f $(DESTDIR)$(BINDIR)/nopcollectd + rm -f $(DESTDIR)$(BINDIR)/nopsysstated + +remove-lib: + rm -f $(DESTDIR)$(LIBDIR)/conf.lua + rm -f $(DESTDIR)$(LIBDIR)/db.lua + rm -f $(DESTDIR)$(LIBDIR)/playlog.lua + rm -f $(DESTDIR)$(LIBDIR)/rddb.lua + rm -f $(DESTDIR)$(LIBDIR)/qlistener.lua + rm -f $(DESTDIR)$(LIBDIR)/tcpserver.lua + +remove-etc: + +purge: remove + rm -rf $(DESTDIR)$(ETCDIR)/rhnop diff --git a/src/conf.lua b/src/conf.lua new file mode 100644 index 0000000..a0af5d3 --- /dev/null +++ b/src/conf.lua @@ -0,0 +1,33 @@ +-- +-- rhnop +-- +-- Copyright (C) 2011-2015 Christian Pointner +-- +-- This file is part of rhnop. +-- +-- rhnop is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- any later version. +-- +-- rhnop 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with rhnop. If not, see . +-- + +local conf = {} + +local file = assert(io.open(conffile, "r")) +for line in file:lines() do + local k,v = string.match(line, "^([^=#]+)=(.*)$") + if k and v and v ~= "" then + conf[k] = v + end +end +file:close() + +return conf diff --git a/src/configure b/src/configure new file mode 100755 index 0000000..0d92d8b --- /dev/null +++ b/src/configure @@ -0,0 +1,223 @@ +#!/bin/sh +# +# rhnop +# +# Copyright (C) 2011-2015 Christian Pointner +# +# This file is part of rhnop. +# +# rhnop is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# any later version. +# +# rhnop 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with rhnop. If not, see . +# + +TARGET=`uname -s` +EBUILD_COMPAT=0 + +CFLAGS='-g -O2' +LDFLAGS='-g -Wall -O2 -lpthread' + +LUA_DIR='' +LUA='' + +PREFIX='/usr/local' +BINDIR='' +ETCDIR='' +LIBDIR='' + +print_usage() { + echo "configure --help print this" + echo " --target= build target i.e. Linux (default: autodetect)" + echo " --prefix= the installation prefix (default: /usr/local)" + echo " --bindir= the path to the bin directory (default: $PREFIX/bin)" + echo " --sysconfdir= the path to the system configuration directory (default: $PREFIX/etc)" + echo " --libdir= the path to the lua library directory (default: $PREFIX/lib/rhnop)" + echo " --with-lua= use this lua tree instead of system default" +} + +for arg +do + case $arg in + --target=*) + TARGET=${arg#--target=} + ;; + --prefix=*) + PREFIX=${arg#--prefix=} + ;; + --bindir=*) + BINDIR=${arg#--bindir=} + ;; + --sysconfdir=*) + ETCDIR=${arg#--sysconfdir=} + ;; + --libdir=*) + LIBDIR=${arg#--libdir=} + ;; + --with-lua=*) + LUA_DIR=${arg#--with-lua=} + ;; + --ebuild-compat) + EBUILD_COMPAT=1 + ;; + --help) + print_usage + exit 0 + ;; + *) + ERRORS="$ERRORS $arg" + ;; + esac +done + +if [ -n "$ERRORS" ] && [ $EBUILD_COMPAT -ne 1 ]; then + for error in $ERRORS; do + echo "Unknown argument: $error" + done + + print_usage + exit 1 +fi + +rm -f config.h +rm -f include.mk +case $TARGET in + Linux) + LDFLAGS=$LDFLAGS' -ldl' + ;; + *) + echo "platform not supported" + exit 1; + ;; +esac + + +test_lua_version() +{ + LUA_VERSION=`cat $1 | grep "#define LUA_VERSION[ ]" | cut -f2- | tr -d '"' | sed -e 's/Lua \([0-9][0-9.]*\)/\1/'` + LUA_VERSION_NUM=`cat $1 | grep "#define LUA_VERSION_NUM" | awk '{ print $3 }'` + LUA_RELEASE=`cat $1 | grep "#define LUA_RELEASE[ ]" | cut -f2-` + + if [ $LUA_VERSION_NUM -ge 501 ]; then + return 1; + else + return 0; + fi +} + +if [ -z "$LUA_DIR" ]; then + for prefix in /usr /usr/local; do + if [ -e $prefix/include/lua.h ]; then + test_lua_version $prefix/include/lua.h + if [ $? -eq 1 ]; then + echo "using Lua $LUA_VERSION ($LUA_RELEASE) found at $prefix/include" + CFLAGS="$CFLAGS -I'$prefix/include'" + LDFLAGS="$LDFLAGS -L'$prefix/lib' -llua" + LUA=$prefix/lua + LUAC=$prefix/luac + break + fi + else + for dir in `ls -d $prefix/include/lua* 2> /dev/null`; do + if [ -e $dir/lua.h ]; then + test_lua_version $dir/lua.h + if [ $? -eq 1 ]; then + echo "using Lua $LUA_VERSION ($LUA_RELEASE) found at $dir" + CFLAGS="$CFLAGS -I$dir" + if [ -x "$prefix/bin/lua$LUA_VERSION" ]; then + LDFLAGS="$LDFLAGS -L'$prefix/lib' -llua$LUA_VERSION" + LUA=$prefix/bin/lua$LUA_VERSION + LUAC=$prefix/bin/luac$LUA_VERSION + elif [ -x "$prefix/bin/lua-$LUA_VERSION" ]; then + LDFLAGS="$LDFLAGS -L'$prefix/lib' -llua-$LUA_VERSION" + LUA=$prefix/bin/lua-$LUA_VERSION + LUAC=$prefix/bin/luac-$LUA_VERSION + else + echo "ERROR: found lua.h at $dir/lua.h but no matching lua and luac" + return 1 + fi + break + fi + fi + done + if [ -n "$LUAC" ]; then + break + fi + fi + done + + if [ -z "$LUAC" ]; then + echo "ERROR: no suitable lua found .. please install lua 5.1 or higher or use --with-lua" + return 1 + fi + +else + CFLAGS="$CFLAGS -I'$LUA_DIR/include'" + LDFLAGS="$LDFLAGS '$LUA_DIR/lib/liblua.a'" + LUA=$LUA_DIR/bin/lua + LUAC=$LUA_DIR/bin/luac +fi + +if [ -z "$BINDIR" ]; then + BINDIR=$PREFIX/bin +fi + +if [ -z "$ETCDIR" ]; then + ETCDIR=$PREFIX/etc +fi + +if [ -z "$LIBDIR" ]; then + LIBDIR=$PREFIX/lib/rhnop +fi + +cat > include.mk < config.h < +-- +-- This file is part of rhnop. +-- +-- rhnop is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- any later version. +-- +-- rhnop 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with rhnop. If not, see . +-- + +luasql = require "luasql.mysql" + +-- for destination database +-- CREATE DATABASE nop CHARACTER SET utf8 COLLATE utf8_unicode_ci; +-- GRANT select,insert,update,delete ON nop.master TO 'nopcollectd'@'localhost' IDENTIFIED BY ''; +-- GRANT select,insert,update,delete ON nop.standby TO 'nopcollectd'@'localhost'; +-- GRANT select,insert,update ON nop.state TO 'nopsysstated'@'localhost' IDENTIFIED BY ''; +-- GRANT select ON nop.* TO 'nop'@'localhost' IDENTIFIED BY ''; +-- USE nop +-- CREATE TABLE IF NOT EXISTS master (timestamp BIGINT UNSIGNED PRIMARY KEY NOT NULL, cart INT NOT NULL, len INT, showtitle VARCHAR(255), title VARCHAR(255), artist VARCHAR(255), album VARCHAR(255), ismusic BOOLEAN); +-- ALTER TABLE master CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci; +-- CREATE TABLE IF NOT EXISTS standby (timestamp BIGINT UNSIGNED PRIMARY KEY NOT NULL, cart INT NOT NULL, len INT, showtitle VARCHAR(255), title VARCHAR(255), artist VARCHAR(255), album VARCHAR(255), ismusic BOOLEAN); +-- ALTER TABLE standby CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci; +-- CREATE TABLE IF NOT EXISTS state (timestamp BIGINT UNSIGNED PRIMARY KEY NOT NULL, state VARCHAR(32)); +-- ALTER TABLE state CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci; + +local db = {} + +function db.init(db, user, pwd, host, port, table) + local mydb = {} + + mydb.table = table + + local err + + mydb.env, err = luasql.mysql() + if mydb.env == nil then + return nil, err + end + + mydb.con, err = mydb.env:connect(db, user, pwd, host, port) + if mydb.con == nil then + return nil, err + end + + local ret, err = mydb.con:execute("SET CHARACTER SET utf8") + if ret == nil then + return nil, err + end + + ret, err = mydb.con:setautocommit(true) + if ret == nil then + return nil, err + end + + function mydb:getLastEntry() + local cur, err = self.con:execute("SELECT MAX(timestamp) FROM " .. self.table) + if cur == nil then + return nil, err + end + + local timestamp = cur:fetch() + if timestamp == nil then timestamp = 0 end + return timestamp + end + + function mydb:findMissingEntries(lasttimestamp) + local lastts = self.con:escape(lasttimestamp) + return self.con:execute("SELECT * FROM " .. self.table .. " WHERE timestamp > " .. lastts) + end + + function mydb:getNextMissingEntry(cur) + local data = {} + data = cur:fetch(data, "a") + if data == nil then + return nil, "that's all folks" + end + return data + end + + function mydb:getEntry(timestamp) + local ts = self.con:escape(timestamp) + local cur, err = self.con:execute("SELECT * FROM " .. self.table .. " WHERE timestamp = " .. ts) + if cur == nil then + return nil, err + end + + local data = {} + data = cur:fetch(data, "a") + if data == nil then + return nil, "nothing found" + end + return data + end + + function mydb:addEntry(data) + local timestamp = self.con:escape(data.timestamp) + local cart = self.con:escape(data.cart) + local len = self.con:escape(data.len) + local showtitle = self.con:escape(data.showtitle) + local title = self.con:escape(data.title) + local artist = self.con:escape(data.artist) + local album = self.con:escape(data.album) + local ismusic = self.con:escape(data.ismusic) + + local cur, err = self.con:execute("REPLACE INTO " .. self.table .. " VALUES('" .. timestamp .. "', '" .. cart .. "', '" .. len .. "', '" .. showtitle .. "', '" .. title .. "', '" .. artist .."', '" .. album .. "', '" .. ismusic .. "')") + if cur == nil then + return nil, err + end + + return true + end + + function mydb:getLastSysState() + local cur, err = self.con:execute("SELECT state FROM " .. self.table .. " WHERE timestamp = (SELECT MAX(timestamp) FROM " .. self.table .. ")") + if cur == nil then + return nil, err + end + + local state = cur:fetch() + if state == nil then state = "unknown" end + return state + end + + function mydb:updateSysState(timestamp, state) + local t = self.con:escape(timestamp) + local s = self.con:escape(state) + + local cur, err = self.con:execute("INSERT INTO " .. self.table .. " VALUES('" .. t .. "', '" .. s .. "')") + if cur == nil then + return nil, err + end + return true + end + + function mydb:close() + if self.con then + self.con:close() + end + + if self.env then + self.env:close() + end + end + + return mydb +end + +return db diff --git a/src/l_pipe.c b/src/l_pipe.c new file mode 100644 index 0000000..3812905 --- /dev/null +++ b/src/l_pipe.c @@ -0,0 +1,137 @@ +/* + * rhnop + * + * Copyright (C) 2011-2015 Christian Pointner + * + * This file is part of rhnop. + * + * rhnop is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * rhnop 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with rhnop. If not, see . + */ + +#include +#include + +#include +#include +#include + +#include "l_pipe.h" + +static int pipefds_[2]; + +int pipe_init() +{ + return pipe(pipefds_); +} + +void pipe_close() +{ + close(pipefds_[0]); + close(pipefds_[1]); +} + +static int l_pipe_signal(lua_State *L) +{ + size_t len = 0, written = 0; + const char* data = luaL_checklstring(L, 1, &len); + len++; // also send trailing zero + + int ret = 0; + for(;;) { + ret = write(pipefds_[1], &(data[written]), len - written); + if(!ret) continue; + if(ret == -1 && (errno == EAGAIN || errno == EINTR)) continue; + + written += ret; + if(written == len) break; + + break; + } + + lua_pushinteger(L, ret); + return 1; +} + +static int l_pipe_getfd(lua_State *L) +{ + if(!lua_istable(L, -1)) + luaL_error(L, "can't retreive pipe fd"); + + lua_pushliteral(L, "fd"); + lua_gettable(L, -2); + return 1; +} + +static int l_pipe_dirty(lua_State *L) +{ + lua_pushboolean(L, 0); + return 1; +} + +static int l_pipe_getreadfd(lua_State *L) +{ + lua_newtable(L); + lua_pushliteral(L, "fd"); + lua_pushinteger(L, pipefds_[0]); + lua_settable(L, -3); + lua_pushliteral(L, "getfd"); + lua_pushcfunction(L, l_pipe_getfd); + lua_settable(L, -3); + lua_pushliteral(L, "dirty"); + lua_pushcfunction(L, l_pipe_dirty); + lua_settable(L, -3); + return 1; +} + +static int l_pipe_consume(lua_State *L) +{ + char data[17]; // 17 should be sufficient + size_t len = 0; + + int ret = 0; + for(;;) { + ret = read(pipefds_[0], &(data[len]), 1); + if(ret == 0) break; + if(ret == -1 && (errno == EAGAIN || errno == EINTR)) continue; + + if(data[len] == 0) + break; + + len += ret; + if(len >= sizeof(data)) { + ret = 0; + break; + } + } + + if(ret) + lua_pushstring(L, data); + else + lua_pushnil(L); + + return 1; +} + +static const struct luaL_reg pipe_funcs [] = { + { "signal", l_pipe_signal }, + { "getreadfd", l_pipe_getreadfd }, + { "consume", l_pipe_consume }, + { NULL, NULL } +}; + +LUALIB_API int luaopen_pipe(lua_State *L) +{ + luaL_register(L, LUA_PIPELIBNAME, pipe_funcs); + return 1; +} diff --git a/src/l_pipe.h b/src/l_pipe.h new file mode 100644 index 0000000..0b67bf8 --- /dev/null +++ b/src/l_pipe.h @@ -0,0 +1,33 @@ +/* + * rhnop + * + * Copyright (C) 2011-2015 Christian Pointner + * + * This file is part of rhnop. + * + * rhnop is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * rhnop 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with rhnop. If not, see . + */ + +#ifndef NOPSYNCD_l_pipe_h_INCLUDED +#define NOPSYNCD_l_pipe_h_INCLUDED + +#include + +int pipe_init(); +void pipe_close(); + +#define LUA_PIPELIBNAME "pipe" +LUALIB_API int luaopen_pipe(lua_State *L); + +#endif diff --git a/src/nopcollectd b/src/nopcollectd new file mode 100755 index 0000000..0f7c66e --- /dev/null +++ b/src/nopcollectd @@ -0,0 +1,75 @@ +#!/usr/bin/lua +-- +-- rhnop +-- +-- Copyright (C) 2011-2014 Christian Pointner +-- +-- This file is part of rhnop. +-- +-- rhnop is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- any later version. +-- +-- rhnop 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with rhnop. If not, see . +-- +-- +-- send now and next cart# and length to nopsyncd +-- this script should be called by the now/next macro cart +-- the arguments should be: now# nowlen next# nextlen +-- + +require "socket" + +package.path = package.path .. ";_rhnoplibdir_/?.lua" +db = require "db" + +if #arg < 1 then + io.stderr:write("too few parameters\n") + os.exit(1) +end + +conffile = "nopcollectd." .. arg[1] .. ".conf" +conf = require "conf" + +src_db = assert(db.init(conf.src_db, conf.src_user, conf.src_pwd, conf.src_host, conf.src_port, conf.src_table)) +dst_db = assert(db.init(conf.dst_db, conf.dst_user, conf.dst_pwd, conf.dst_host, conf.dst_port, conf.dst_table)) + +local sock = assert(socket.tcp()) +local ret, err = sock:connect(conf.sync_host, conf.sync_port) +if ret == nil then + print(err) + return 1 +end +print "connection established" + +local last = dst_db:getLastEntry() +print("last timestamp was " .. last) +local cur = assert(src_db:findMissingEntries(last)) +local cnt = 0 +while true do + local data = src_db:getNextMissingEntry(cur) + if data == nil then break end + local ret, err = dst_db:addEntry(data) + if ret == nil then print(err) end + cnt = cnt + 1 +end + +print("synced " .. cnt .. " Entries") + +while true do + local timestamp = sock:receive('*l') + if timestamp == nil then break end + data = assert(src_db:getEntry(timestamp)) + assert(dst_db:addEntry(data)) +end + +sock:close() + +return 0 diff --git a/src/nopcollectd.master.conf b/src/nopcollectd.master.conf new file mode 100644 index 0000000..5d61551 --- /dev/null +++ b/src/nopcollectd.master.conf @@ -0,0 +1,16 @@ +sync_host=airplay.helsinki.at +sync_port=2345 + +src_db=rhnop +src_table=now +src_host=airplay.helsinki.at +#src_port=3306 +src_user=nopcollectd +src_pwd=123456 + +dst_db=nop +dst_table=master +dst_host=127.0.0.1 +#dst_port=3306 +dst_user=nopcollectd +dst_pwd=123456 diff --git a/src/nopcollectd.standby.conf b/src/nopcollectd.standby.conf new file mode 100644 index 0000000..92ba1aa --- /dev/null +++ b/src/nopcollectd.standby.conf @@ -0,0 +1,16 @@ +sync_host=airplay2.helsinki.at +sync_port=2345 + +src_db=rhnop +src_table=now +src_host=airplay2.helsinki.at +#src_port=3306 +src_user=nopcollectd +src_pwd=123456 + +dst_db=nop +dst_table=standby +dst_host=127.0.0.1 +#dst_port=3306 +dst_user=nopcollectd +dst_pwd=123456 diff --git a/src/noprml b/src/noprml new file mode 100755 index 0000000..739e56a --- /dev/null +++ b/src/noprml @@ -0,0 +1,64 @@ +#!/usr/bin/lua +-- +-- rhnop +-- +-- Copyright (C) 2011-2015 Christian Pointner +-- +-- This file is part of rhnop. +-- +-- rhnop is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- any later version. +-- +-- rhnop 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with rhnop. If not, see . +-- +-- +-- send now and next cart# and length to nopsyncd +-- this script should be called by the now/next macro cart +-- the arguments should be: now# nowlen next# nextlen +-- + +local queue_name = "/rhnop" + +-- reading timestamp (milliseconds since epoch) +local p = assert(io.popen("/bin/date --utc '+%s %N'" , 'r')) +local time = assert(p:read('*l')) +p:close() +local s, ns = assert(string.match(time, "([0-9]+) ([0-9]+)")) +local timestamp = s .. string.format("%06d", math.floor(ns/1000)) + +-- check arguments +if #arg < 4 then + io.stderr:write("too few parameters\n") + os.exit(1) +end + +require "posix" +mq = require "mq" + +-- open message queue +posix.umask("rwxrwxr-x") +local q, err = mq.create(queue_name, "wo", "rw-rw----") +if q == nil then + q, err = mq.open(queue_name, "wo") + if q == nil then + io.stderr:write("creation of message queue failed: " .. err .. "\n") + os.exit(1) + end +end + +-- send out message to nopsyncd +local result, err = mq.send(q, timestamp .. " " .. arg[1] .. " " .. arg[2] .. " " .. arg[3] .. " " .. arg[4], 0) +if result == nil then + io.stderr:write("sending message failed: " .. err .. "\n") + os.exit(2) +end + +mq.close(q) diff --git a/src/nopsyncd.c b/src/nopsyncd.c new file mode 100644 index 0000000..8a059d0 --- /dev/null +++ b/src/nopsyncd.c @@ -0,0 +1,171 @@ +/* + * rhnop + * + * Copyright (C) 2011-2015 Christian Pointner + * + * This file is part of rhnop. + * + * rhnop is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * rhnop 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with rhnop. If not, see . + */ + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "l_pipe.h" +#include "config.h" + +#define LUA_MAIN_LOOP_FUNC "main_loop" +#define QLISTENER LIBDIR"/qlistener.lua" +#define TCPSERVER LIBDIR"/tcpserver.lua" + +static const luaL_Reg nopsyncd_lualibs[] = { + {"", luaopen_base}, + {LUA_LOADLIBNAME, luaopen_package}, + {LUA_TABLIBNAME, luaopen_table}, + {LUA_STRLIBNAME, luaopen_string}, + {LUA_MATHLIBNAME, luaopen_math}, + {LUA_IOLIBNAME, luaopen_io}, + {LUA_OSLIBNAME, luaopen_os}, + {LUA_PIPELIBNAME, luaopen_pipe}, + {NULL, NULL} +}; + +int init_main_loop(lua_State *L, const char* filename) +{ + const luaL_Reg *lib = nopsyncd_lualibs; + for (; lib->func; lib++) { + lua_pushcfunction(L, lib->func); + lua_pushstring(L, lib->name); + lua_call(L, 1, 0); + } + + int ret = luaL_loadfile(L, filename); + if(ret) { + const char* err_str = luaL_checkstring(L, -1); + switch(ret) { + case LUA_ERRSYNTAX: fprintf(stderr, "luaL_loadfile() syntax error: %s\n", err_str); break; + case LUA_ERRMEM: fprintf(stderr, "luaL_loadfile() malloc error: %s\n", err_str); break; + case LUA_ERRFILE: fprintf(stderr, "lauL_loadfile() error: %s\n", err_str); break; + default: fprintf(stderr, "luaL_loadfile() unknown error: %s\n", err_str); break; + } + return -1; + } + + lua_pushstring(L, LIBDIR); + lua_setglobal(L, "rhnoplibdir"); + + ret = lua_pcall(L, 0, 0, 0); + if(ret) { + const char* err_str = luaL_checkstring(L, -1); + switch(ret) { + case LUA_ERRRUN: fprintf(stderr, "lua_pcall() runtime error: %s\n", err_str); break; + case LUA_ERRMEM: fprintf(stderr, "lua_pcall() malloc error: %s\n", err_str); break; + case LUA_ERRERR: fprintf(stderr, "lua_pcall() error at error handler function: %s\n", err_str); break; + } + return -1; + } + + return 0; +} + +int call_main_loop(lua_State* L, const char* filename) +{ + lua_getglobal(L, LUA_MAIN_LOOP_FUNC); + if(!lua_isfunction(L, -1)) { + fprintf(stderr, "there is no function '%s' at file '%s'\n", LUA_MAIN_LOOP_FUNC, filename); + return -1; + }; + + int ret = lua_pcall(L, 0, 1, 0); + if(ret) { + const char* err_str = luaL_checkstring(L, -1); + switch(ret) { + case LUA_ERRRUN: fprintf(stderr, "lua_pcall(%s:%s) runtime error: %s\n", filename, LUA_MAIN_LOOP_FUNC, err_str); break; + case LUA_ERRMEM: fprintf(stderr, "lua_pcall(%s:%s) malloc error: %s\n", filename, LUA_MAIN_LOOP_FUNC, err_str); break; + case LUA_ERRERR: fprintf(stderr, "lua_pcall(%s:%s) error at error handler function: %s\n", filename, LUA_MAIN_LOOP_FUNC, err_str); break; + } + return -1; + } + + ret = lua_tointeger(L, 1); + return ret; +} + +void* main_loop(void* file) +{ + if(!file) + pthread_exit(NULL); + + + lua_State *L; + L = luaL_newstate(); + if(!L) { + fprintf(stderr, "error creating lua state\n"); + pthread_exit(NULL); + } + + int ret = init_main_loop(L, (char*)file); + if(!ret) + ret = call_main_loop(L, (char*)file); + + printf("%s returned with %d\n", (char*)file, ret); + + lua_close(L); + + /* this should bring down the other thread as well + at least this is true for the tcp-server thread */ + pipe_close(); + + pthread_exit(NULL); +} + +int main(int argc, char* argv[]) +{ + printf("starting nopsyncd...\n"); + + pthread_t qlistener, tcpserver; + + int ret = pipe_init(); + if(ret) { + fprintf(stderr, "Error creating pipe: %s\n", strerror(errno)); + return 1; + } + + ret = pthread_create(&qlistener, NULL, main_loop, QLISTENER); + if(ret) { + fprintf(stderr, "Error creating qlistener thread (code: %d)\n", ret); + return 1; + } + pthread_detach(qlistener); /* can't kill this thread so don't join to it */ + + ret = pthread_create(&tcpserver, NULL, main_loop, TCPSERVER); + if(ret) { + fprintf(stderr, "Error creating tcpserver thread (code: %d)\n", ret); + return 1; + } + +/* this thread can't be cancelled so don't wait for it */ +/* pthread_join(qlistener, NULL); */ + pthread_join(tcpserver, NULL); + + printf("stopping nopsyncd.\n"); + return 0; +} diff --git a/src/nopsyncd.conf b/src/nopsyncd.conf new file mode 100644 index 0000000..a80d620 --- /dev/null +++ b/src/nopsyncd.conf @@ -0,0 +1,18 @@ +queue_name=/rhnop +tcp_host=* +tcp_port=2345 + +music_carts_lo=400000 +music_carts_hi=499999 + +rddb_db=rivendell +rddb_host=127.0.0.1 +#rddb_port=3306 +rddb_user= +rddb_pwd= + +playlog_db=rhnop +playlog_host=127.0.0.1 +#playlog_port=3306 +playlog_user= +playlog_pwd= diff --git a/src/nopsysstated b/src/nopsysstated new file mode 100755 index 0000000..f1a05f8 --- /dev/null +++ b/src/nopsysstated @@ -0,0 +1,71 @@ +#!/usr/bin/lua +-- +-- rhnop +-- +-- Copyright (C) 2011-2015 Christian Pointner +-- +-- This file is part of rhnop. +-- +-- rhnop is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- any later version. +-- +-- rhnop 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with rhnop. If not, see . +-- +-- +-- send now and next cart# and length to nopsyncd +-- this script should be called by the now/next macro cart +-- the arguments should be: now# nowlen next# nextlen +-- + +require "socket" + +package.path = package.path .. ";_rhnoplibdir_/?.lua" +db = require "db" + +conffile = "nopsysstated.conf" +conf = require "conf" + +db = assert(db.init(conf.db_db, conf.db_user, conf.db_pwd, conf.db_host, conf.db_port, conf.db_table)) + +local sock = assert(socket.tcp()) +local ret, err = sock:connect(conf.host, conf.port) +if ret == nil then + print(err) + return 1 +end +print "connection established" + +local current_state, err = db:getLastSysState() +if current_state == nil then + print(err) + return 1 +end +print("current state according to db is " .. current_state) + +while true do + local state = sock:receive('*l') + if state == nil then break end + if state ~= current_state then + -- reading timestamp (milliseconds since epoch) + local p = assert(io.popen("/bin/date --utc '+%s %N'" , 'r')) + local time = assert(p:read('*l')) + p:close() + local s, ns = assert(string.match(time, "([0-9]+) ([0-9]+)")) + local timestamp = s .. string.format("%06d", math.floor(ns/1000)) + + assert(db:updateSysState(timestamp, state)) + current_state = state + end +end + +sock:close() + +return 0 diff --git a/src/nopsysstated.conf b/src/nopsysstated.conf new file mode 100644 index 0000000..b9892c8 --- /dev/null +++ b/src/nopsysstated.conf @@ -0,0 +1,9 @@ +host=rhctl.helsinki.at +port=2345 + +db_db=nop +db_table=state +db_host=127.0.0.1 +#db_port=3306 +db_user=nopsysstated +db_pwd=123456 diff --git a/src/playlog.lua b/src/playlog.lua new file mode 100644 index 0000000..33095c4 --- /dev/null +++ b/src/playlog.lua @@ -0,0 +1,98 @@ +-- +-- rhnop +-- +-- Copyright (C) 2011-2015 Christian Pointner +-- +-- This file is part of rhnop. +-- +-- rhnop is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- any later version. +-- +-- rhnop 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with rhnop. If not, see . +-- + +luasql = require "luasql.mysql" + +-- CREATE DATABASE rhnop CHARACTER SET utf8 COLLATE utf8_unicode_ci; +-- GRANT select,insert,update ON rhnop.* TO 'nopsyncd' IDENTIFIED BY ''; +-- GRANT select ON rhnop.* TO 'nopcollectd' IDENTIFIED BY ''; +-- USE rhnop +-- CREATE TABLE IF NOT EXISTS now (timestamp BIGINT UNSIGNED PRIMARY KEY NOT NULL, cart INT NOT NULL, len INT, showtitle VARCHAR(255), title VARCHAR(255), artist VARCHAR(255), album VARCHAR(255), ismusic BOOLEAN); +-- ALTER TABLE now CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci; + +conf = require "conf" + +local playlog = {} + +function playlog:init() + local err + + self.env, err = luasql.mysql() + if self.env == nil then + return nil, err + end + + self.con, err = self.env:connect(conf.playlog_db, conf.playlog_user, conf.playlog_pwd, conf.playlog_host, conf.playlog_port) + if self.con == nil then + return nil, err + end + + local ret, err = self.con:execute("SET CHARACTER SET utf8") + if ret == nil then + return nil, err + end + + ret, err = self.con:setautocommit(true) + if ret == nil then + return nil, err + end + + return true +end + +function playlog:getLastCart() + local cur, err = self.con:execute("SELECT cart FROM now WHERE timestamp = (SELECT MAX(timestamp) FROM now)") + if cur == nil then + return nil, err + end + + local cart = cur:fetch() + if cart == nil then cart = 0 end + return cart +end + +function playlog:insertNowMusic(timestamp, cart, len, title, artist, album) + cart = tonumber(cart) + len = tonumber(len) + if cart < 400000 or cart > 450000 then + poolnum = 0 + else + poolnum = math.floor(cart/1000) - 399 + end + local cur, err = self.con:execute("INSERT into now VALUES(" .. self.con:escape(timestamp) .. ", " .. cart .. ", " .. len .. ", 'Musikpool " .. poolnum .. "', '" .. self.con:escape(title) .. "', '" .. self.con:escape(artist) .."', '" .. self.con:escape(album) .. "', 1)") + if cur == nil then + return nil, err + end + + return true +end + +function playlog:close() + if self.con then + self.con:close() + end + + if self.env then + self.env:close() + end +end + +return playlog diff --git a/src/qlistener.lua b/src/qlistener.lua new file mode 100644 index 0000000..e3b774b --- /dev/null +++ b/src/qlistener.lua @@ -0,0 +1,109 @@ +-- +-- rhnop +-- +-- Copyright (C) 2011-2015 Christian Pointner +-- +-- This file is part of rhnop. +-- +-- rhnop is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- any later version. +-- +-- rhnop 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with rhnop. If not, see . +-- + +local last_cart = nil + +require "posix" +mq = require "mq" + +package.path = package.path .. ";" .. rhnoplibdir .. "/?.lua" +playlog = require "playlog" +rddb = require "rddb" + +conffile = "nopsyncd.conf" +conf = require "conf" +if conf.music_carts_lo == nil then + conf.music_carts_lo = 400000 +end +if conf.music_carts_hi == nil then + conf.music_carts_hi = 499999 +end +conf.music_carts_lo = tonumber(conf.music_carts_lo) +conf.music_carts_hi = tonumber(conf.music_carts_hi) + + +function handle_now(timestamp, nowcart, nowlen) + local results, err = rddb:getCartInfo(nowcart); + if results == nil then + io.stderr:write("can't fetch cart info: " .. err .. "\n") +-- TODO: this error shouldn't be ignored!!!! + else +-- print(timestamp .. " Info: '" .. results.TITLE .. "' von '" .. results.ARTIST .. "' aus '" .. results.ALBUM .. "'") + local ret, err = playlog:insertNowMusic(timestamp, nowcart, nowlen, results.TITLE, results.ARTIST, results.ALBUM) + if ret == nil then + io.stderr:write("can't insert music info: " .. err .. "\n") + else + pipe.signal(timestamp) + end + end +end + +function handle_message(msg) + local timestamp, nowcart, nowlen, nextcart, nextlen = string.match(msg, "^(%d+) (%d+) (%d+) (%d+) (%d+)$"); + if not timestamp or not nowcart or not nowlen or not nextcart or not nextlen then + io.stderr:write("ignoring malformed message\n") + else + nowcart = tonumber(nowcart) + nowlen = tonumber(nowlen) + if last_cart ~= nowcart and nowcart >= conf.music_carts_lo and nowcart <= conf.music_carts_hi and nowlen > 0 then + last_cart = nowcart + handle_now(timestamp, nowcart, nowlen) + end + -- TODO handle next info + end +end + +function main_loop() + posix.umask("rwxrwxr-x") + local q, err = mq.create(conf.queue_name, "ro", "rw-rw----") + if q == nil then + q, err = mq.open(conf.queue_name, "wo") + if q == nil then + io.stderr:write("creation of message queue failed: " .. err .. "\n") + os.exit(1) + end + end + + local ret, err = playlog:init() + if ret == nil then + io.stderr:write("creation of playlog failed: " .. err .. "\n") + os.exit(1) + end + last_cart = assert(playlog:getLastCart()) + + local ret, err = rddb:init() + if ret == nil then + io.stderr:write("opening rivendell db failed: " .. err .. "\n") + playlog:close() + os.exit(1) + end + + while true do + local msg, prio = mq.receive(q) + if msg == nil then + io.stderr:write("recv error: " .. prio .. "\n") + rddb:close() + playlog:close() + os.exit(2) + end + handle_message(msg) + end +end diff --git a/src/rddb.lua b/src/rddb.lua new file mode 100644 index 0000000..52ecf1b --- /dev/null +++ b/src/rddb.lua @@ -0,0 +1,78 @@ +-- +-- rhnop +-- +-- Copyright (C) 2011-2015 Christian Pointner +-- +-- This file is part of rhnop. +-- +-- rhnop is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- any later version. +-- +-- rhnop 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with rhnop. If not, see . +-- + +luasql = require "luasql.mysql" + +local rddb = {} + +function rddb:init(conf) + local err + + self.env, err = luasql.mysql() + if self.env == nil then + return nil, err + end + + self.con, err = self.env:connect(conf.rddb_db, conf.rddb_user, conf.rddb_pwd, conf.rddb_host, conf.rddb_port) + if self.con == nil then + return nil, err + end + + local ret, err = self.con:execute("SET CHARACTER SET utf8") + if ret == nil then + return nil, err + end + + return true +end + +function rddb:getCartInfo(cartnum) + local cur, err = self.con:execute("select TITLE,ARTIST,ALBUM from CART where NUMBER = " .. self.con:escape(cartnum)) + if cur == nil then + return nil, err + end + + if cur:numrows() ~= 1 then + return nil, "nothing found in rivendell db" + end + + local results = {} + results, err = cur:fetch(results, "a") + cur:close() + + if results.TITLE == nil then results.TITLE = "" end + if results.ARTIST == nil then results.ARTIST = "" end + if results.ALBUM == nil then results.ALBUM = "" end + + return results, err +end + +function rddb:close() + if self.con then + self.con:close() + end + + if self.env then + self.env:close() + end +end + +return rddb diff --git a/src/tcpserver.lua b/src/tcpserver.lua new file mode 100644 index 0000000..85992c3 --- /dev/null +++ b/src/tcpserver.lua @@ -0,0 +1,133 @@ +-- +-- rhnop +-- +-- Copyright (C) 2011-2015 Christian Pointner +-- +-- This file is part of rhnop. +-- +-- rhnop is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- any later version. +-- +-- rhnop 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with rhnop. If not, see . +-- + +require "socket" + +package.path = package.path .. ";" .. rhnoplibdir .. "/?.lua" +conf = require "conf" + +function init_server() + local server = assert(socket.tcp()) + + assert(server:setoption('reuseaddr', true)) + assert(server:bind(conf.tcp_host, conf.tcp_port)) + assert(server:listen(5)) + + return server +end + +local clients = {} + +function add_client(hdl) + -- print("new client(" .. hdl:getfd() .. ") from " .. hdl:getpeername()) + local client = {} + client.buffer = "" + client.hdl = hdl + client.getfd = function() return hdl:getfd() end + client.dirty = function() return hdl:dirty() end + table.insert(clients, client) +end + +function remove_client(c) + local idx = 0 + for idx, client in ipairs(clients) do + if client == c then + break + end + end + + -- print("removing client(" .. c.hdl:getfd() .. ")") + c.hdl:close() + table.remove(clients, idx) +end + +function cleanup_clients() + for _, client in ipairs(clients) do + client:close() + end +end + +function clients_get_writeables() + local fds = {} + + for _, client in ipairs(clients) do + if client.buffer ~= "" then + table.insert(fds, client) + end + end + + return fds +end + +function clients_senddata(timestamp) + for _, client in ipairs(clients) do + client.buffer = client.buffer .. timestamp .. "\n" + end +end + +function main_loop() + local pipefd = pipe.getreadfd() + + server = init_server() + while true do + local readables, writeables, err = socket.select({ pipefd , server , unpack(clients) } , clients_get_writeables()) + if err then + print("select returned with error: " .. err) + return -1 + else + for _, input in ipairs(readables) do + if input == pipefd then + local timestamp = pipe.consume() + if timestamp == nil then + return 2 + end + -- print("pipe was signaled with timestamp: " .. timestamp) + clients_senddata(timestamp) + elseif input == server then + local client = assert(server:accept()) + add_client(client) + else + if input.hdl then + -- receive is insanely stupid, therefore we must always read one byte only + local _, err = input.hdl:receive(1) + if err then + remove_client(input) + end + end + end + end + + for _, output in ipairs(writeables) do + local ret = output.hdl:send(output.buffer) + if(ret == nil) then + remove_client(output) + else + output.buffer = string.sub(output.buffer, ret+1) + end + end + end + end + + server:close() + cleanup_clients() + + return 0 +end -- cgit v0.10.2