/* * 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 "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 8 /* saves state periodically */ void server_save_state(bool force) { 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_cleanup(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); /* 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; 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); } 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 */ if(pipe2(readpipe, O_DIRECT) < 0) error("error creating pipe, need linux kernel >= 3.4"); if(pipe2(outpipe, O_DIRECT) < 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(); debugf("Child with PID %d spawned\n", getpid()); 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; 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"); 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("FIXME: usage message"); 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("*** Starting 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); server_socket = server_bind(); userdb_init(USERFILE); check_userfile(); load_worldfile(); reqmap_init(); /* save some time after a fork() */ client_init(); /* this initial size 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); struct ev_loop *loop = ev_default_loop(0); /* we initialize signals after creating the default event loop * because libev grabs SIGCHLD */ 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_cleanup); /* everything's ready, hand it over to libev */ ev_loop(loop, 0); /* should never get here */ error("FIXME: unexpected termination"); }