/*
* NetCosm - a MUD server
* Copyright (C) 2016 Franklin Wei
*
* This program 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
* (at your option) any later version.
*
* This program 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 this program. If not, see .
*/
#include "globals.h"
#include "client.h"
#include "hash.h"
#include "server.h"
#include "server_reqs.h"
#include "userdb.h"
#include "util.h"
#include "world.h"
#define DEFAULT_PORT 1234
#define BACKLOG 512
/* global data */
bool are_child = false;
void *child_map = NULL;
/* assume int is atomic */
volatile int num_clients = 0;
/* local data */
static uint16_t port = DEFAULT_PORT;
static int server_socket;
#define SAVE_INTERVAL 10
/* saves state periodically */
void server_save_state(bool force)
{
if(!are_child)
{
static int n = 0;
n = (n + 1) % SAVE_INTERVAL;
if(!n || force)
{
world_save(WORLDFILE);
userdb_write(USERFILE);
}
}
}
static void free_child_data(void *ptr)
{
struct child_data *child = ptr;
if(child->user)
{
free(child->user);
child->user = NULL;
}
if(child->io_watcher)
{
ev_io_stop(EV_DEFAULT_ child->io_watcher);
free(child->io_watcher);
child->io_watcher = NULL;
}
free(ptr);
}
static void handle_disconnects(void)
{
int saved_errno = errno;
pid_t pid;
while((pid = waitpid(-1, NULL, WNOHANG)) > 0)
{
struct child_data *child = hash_lookup(child_map, &pid);
debugf("Client disconnect.\n");
room_user_del(child->room, child);
--num_clients;
hash_remove(child_map, &pid);
}
errno = saved_errno;
}
volatile sig_atomic_t reap_children = 0;
static void sigchld_handler(int sig)
{
(void) sig;
reap_children = 1;
}
static void handle_client(int fd, struct sockaddr_in *addr,
int nclients, int to, int from)
{
client_main(fd, addr, nclients, to, from);
}
static void __attribute__((noreturn)) server_shutdown(void)
{
if(!are_child)
debugf("Shutdown server.\n");
else
debugf("Shutdown worker.\n");
if(shutdown(server_socket, SHUT_RDWR) > 0)
error("shutdown");
close(server_socket);
/* save state */
if(!are_child)
server_save_state(true);
/* shut down modules */
client_shutdown();
obj_shutdown();
reqmap_free();
userdb_shutdown();
verb_shutdown();
world_free();
/* free internal data structures */
hash_free(child_map);
child_map = NULL;
extern void *dir_map;
hash_free(dir_map);
dir_map = NULL;
extern char *current_user;
if(current_user)
free(current_user);
/* shut down libev */
ev_default_destroy();
_exit(0);
}
static void __attribute__((noreturn)) sigint_handler(int s)
{
(void) s;
exit(0);
}
static bool autoconfig = false;
static const char *autouser, *autopass;
static void check_userfile(void)
{
if(access(USERFILE, F_OK) < 0 || userdb_size() == 0)
{
if(!autoconfig)
first_run_setup();
else
auth_user_add(autouser, autopass, PRIV_ADMIN);
userdb_write(USERFILE);
}
if(access(USERFILE, R_OK | W_OK) < 0)
error("cannot access "USERFILE);
}
static void load_worldfile(void)
{
extern const struct roomdata_t netcosm_world[];
extern const size_t netcosm_world_sz;
extern const char *netcosm_world_name;
if(access(WORLDFILE, F_OK) < 0)
{
world_init(netcosm_world, netcosm_world_sz, netcosm_world_name);
world_save(WORLDFILE);
}
else if(access(WORLDFILE, R_OK | W_OK) < 0)
error("cannot access "WORLDFILE);
else
if(!world_load(WORLDFILE, netcosm_world, netcosm_world_sz, netcosm_world_name))
error("Failed to load world from disk.\nTry removing "WORLDFILE".");
}
static int server_bind(void)
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock<0)
error("socket");
int tmp = 1;
if(setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &tmp, sizeof tmp) < 0)
error("setsockopt");
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sock, (struct sockaddr*) &addr, sizeof addr) < 0)
error("bind");
if(listen(sock, BACKLOG) < 0)
error("listen");
return sock;
}
static void childreq_cb(EV_P_ ev_io *w, int revents)
{
(void) EV_A;
(void) w;
/* data from a child's pipe */
if(revents & EV_READ)
{
if(!handle_child_req(w->fd))
{
handle_disconnects();
}
}
if(reap_children)
handle_disconnects();
}
static void new_connection_cb(EV_P_ ev_io *w, int revents)
{
(void) EV_A;
(void) w;
(void) revents;
struct sockaddr_in client;
socklen_t client_len = sizeof(client);
int new_sock = accept(server_socket, (struct sockaddr*) &client, &client_len);
if(new_sock < 0)
error("accept");
++num_clients;
int readpipe[2]; /* child->parent */
int outpipe [2]; /* parent->child */
/* try several methods to create a packet pipe between the child and master: */
/* first try creating a pipe in "packet mode": see pipe(2) */
if(pipe2(readpipe, O_DIRECT) < 0)
{
/* then try a SOCK_SEQPACKET socket pair: see unix(7) */
if(socketpair(AF_UNIX, SOCK_SEQPACKET, 0, readpipe) < 0)
{
/* if that failed, try a SOCK_DGRAM socket as a last resort */
if(socketpair(AF_UNIX, SOCK_DGRAM, 0, readpipe) < 0)
error("couldn't create child-master communication pipe");
else
debugf("WARNING: Using a SOCK_DGRAM socket pair for IPC, performance may be degraded.\n");
}
}
if(pipe2(outpipe, O_DIRECT) < 0)
{
if(socketpair(AF_UNIX, SOCK_SEQPACKET, 0, outpipe) < 0)
{
if(socketpair(AF_UNIX, SOCK_DGRAM, 0, outpipe) < 0)
error("error creating pipe, need linux kernel >= 3.4");
}
}
pid_t pid = fork();
if(pid < 0)
error("fork");
if(!pid)
{
/* child */
are_child = true;
/* close our file descriptors */
close(readpipe[0]);
close(outpipe[1]);
close(server_socket);
/* shut down modules */
obj_shutdown();
reqmap_free();
userdb_shutdown();
verb_shutdown();
world_free();
/* free our data structures */
hash_free(child_map);
child_map = NULL;
/* shut down libev */
ev_default_destroy();
server_socket = new_sock;
handle_client(new_sock, &client, num_clients, readpipe[1], outpipe[0]);
exit(0);
}
else
{
/* parent */
close(readpipe[1]);
close(outpipe[0]);
close(new_sock);
/* add the child to the child map */
struct child_data *new = calloc(1, sizeof(struct child_data));
memcpy(new->outpipe, outpipe, sizeof(outpipe));
memcpy(new->readpipe, readpipe, sizeof(readpipe));
new->addr = client.sin_addr;
new->pid = pid;
new->state = STATE_INIT;
new->user = NULL;
ev_io *new_io_watcher = calloc(1, sizeof(ev_io));
ev_io_init(new_io_watcher, childreq_cb, new->readpipe[0], EV_READ);
ev_set_priority(new_io_watcher, EV_MINPRI);
ev_io_start(EV_A_ new_io_watcher);
new->io_watcher = new_io_watcher;
pid_t *pidbuf = malloc(sizeof(pid_t));
*pidbuf = pid;
hash_insert(child_map, pidbuf, new);
}
}
static void init_signals(void)
{
struct sigaction sa;
/* SIGINT and SIGTERM cause graceful shutdown */
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask, SIGINT);
sa.sa_handler = sigint_handler;
sa.sa_flags = SA_RESTART;
if(sigaction(SIGINT, &sa, NULL) < 0)
error("sigaction");
if(sigaction(SIGTERM, &sa, NULL) < 0)
error("sigaction");
/* ignore SIGPIPE */
sigemptyset(&sa.sa_mask);
sa.sa_handler = SIG_IGN;
sa.sa_flags = 0;
if(sigaction(SIGPIPE, &sa, NULL) < 0)
error("sigaction");
/* libev's default SIGCHLD handler exhibits some really strange
* behavior, which we don't like, so we use our own */
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask, SIGCHLD);
sa.sa_handler = sigchld_handler;
sa.sa_flags = SA_RESTART;
if(sigaction(SIGCHLD, &sa, NULL) < 0)
error("sigaction");
}
static void parse_args(int argc, char *argv[])
{
for(int i = 1; i < argc; ++i)
{
if(argv[i][0] == '-')
{
if(strlen(argv[i]) > 1)
{
char c = argv[i][1];
retry:
switch(c)
{
case 'h': /* help */
debugf("Usage: %s [-d PREFIX] [-a ]\n", argv[0]);
exit(0);
case 'a': /* automatic first-run config */
autoconfig = true;
autouser = argv[++i];
autopass = argv[++i];
break;
case 'd': /* set data prefix */
chdir(argv[++i]);
break;
default:
c = 'h';
goto retry;
}
}
}
else
port = strtol(argv[i], NULL, 10);
}
}
static SIMP_HASH(pid_t, pid_hash);
static SIMP_EQUAL(pid_t, pid_equal);
static void check_libs(void)
{
debugf("*** NetCosm %s (libev %d.%d, %s) ***\n",
NETCOSM_VERSION, EV_VERSION_MAJOR, EV_VERSION_MINOR, OPENSSL_VERSION_TEXT);
assert(ev_version_major() == EV_VERSION_MAJOR &&
ev_version_minor() >= EV_VERSION_MINOR);
}
int server_main(int argc, char *argv[])
{
check_libs();
parse_args(argc, argv);
userdb_init(USERFILE);
/* also performs first-time setup: */
check_userfile();
load_worldfile();
/* initialize request map */
reqmap_init();
/* save some time after a fork() */
client_init();
/* this initial size is set very low to make iteration faster */
child_map = hash_init(16, pid_hash, pid_equal);
hash_setfreedata_cb(child_map, free_child_data);
hash_setfreekey_cb(child_map, free);
debugf("Listening on port %d.\n", port);
server_socket = server_bind();
struct ev_loop *loop = ev_default_loop(0);
/* we initialize signals after creating the default event loop
* because libev grabs SIGCHLD in the process */
init_signals();
ev_io server_watcher;
ev_io_init(&server_watcher, new_connection_cb, server_socket, EV_READ);
ev_set_priority(&server_watcher, EV_MAXPRI);
ev_io_start(EV_A_ &server_watcher);
atexit(server_shutdown);
/* everything's ready, hand it over to libev */
ev_loop(loop, 0);
/* should never get here */
error("FIXME: unexpected termination");
}