From: Christian Heller Date: Mon, 17 Mar 2014 03:23:23 +0000 (+0100) Subject: Re-wrote large parts of the server client architecture. No more fifo. X-Git-Tag: tce~822 X-Git-Url: https://plomlompom.com/repos/%7B%7Bprefix%7D%7D/static/%7B%7B%20web_path%20%7D%7D/decks/task?a=commitdiff_plain;h=d12efb0addf420adc045cfb96647dff6241310ee;p=plomrogue Re-wrote large parts of the server client architecture. No more fifo. server/out in its old use moves to server/worldstate, while the new server/out is used for specific server messages. server/in is now a plain text file. Solved the problem of parallel server processes, too. --- diff --git a/README b/README index c44d7da..7963f62 100644 --- a/README +++ b/README @@ -5,18 +5,17 @@ plomlompom tries to build his own roguelike. It doesn't do much yet (although plomlompom has insanely ambitious long-term plans). You can move around a player on an island and meet different enemies. You have 5 -hitpoints to lose before death; they start with different amounts of hitpoints, -depending on their species. Dead enemies become dirt, skeletons or "magic meat" --- such objects can be collected, and "magic meat" can be consumed to gain -hitpoints. Note that different kinds of movements/actions take different numbers -of turns to finish. +hitpoints to lose before death. Enemies start with different amounts of +hitpoints, depending on their species. Dead enemies become dirt, skeletons or +"magic meat" -- such objects can be collected, and "magic meat" can be consumed +to gain hitpoints. Note that different kinds of movements/actions take different +numbers of turns to finish. Enemies' AI is very dumb so far: Each turn, they try to move towards their -path-wise nearest enemy. If no enemy is found in their surroundings, they just -wait. +shortest-path-wise nearest enemy. If no enemy is found in their surroundings, +they just wait. -Note that diagonal movement is possible, but takes (40%) longer than orthogonal -movement. +Diagonal movement is possible, but takes (40%) longer than orthogonal movement. Once you start a new world, every move of yours is recorded in a file called "record". Once you re-start the game, all of your previous moves are replayed @@ -36,10 +35,10 @@ $ ./roguelike (It may also work on other Unix-like systems with ncurses, who knows.) -Note that make generates two executables ./roguelike-server and -./roguelike-client. ./roguelike is a pre-existing shell script that merely -executes both of them, with the server as a background job. You can also -ignore the script and start the two by hand. +Make generates two executables ./roguelike-server and ./roguelike-client. +./roguelike is a pre-existing shell script that merely executes both of them, +with the server as a background job. You can also ignore the script and start +the two by hand. Client's keybindings and window management ------------------------------------------ @@ -103,7 +102,8 @@ All source files are thoroughly documented to explain more details of plomrogue's internals. The ./roguelike-server executable can be run with a -v option for helpful debugging info (mostly: what messages the client sends to the server). Server and client communicate via files in the ./server/ directory -(generated when the server is first run). The ./server/in fifo receives commands -for the server as null-terminated strings. The ./server/out file contains a -serialized representation of the game world's data as it is to be available to +(generated when the server is first run). The ./server/in file is read by the +server for newline-delimited commands. The ./server/out file contains server +messages to be read by clients. The ./server/worldstate file contains a +serialized representation of the game world's data as it is to be visible to the player / the player's client. diff --git a/TODO b/TODO index 766b12d..4c9b894 100644 --- a/TODO +++ b/TODO @@ -2,19 +2,15 @@ Next planned steps in plomrogue development: BOTH SERVER/CLIENT: -- implement better server/client architecture, meeting these criteria: - - keep interaction of server and client in the filesystem (as currently - happens by use of plain text files and fifos) to allow for easy scripting - - allow for specific requests of the client for specific server data (such as: - "what path do you propose for the player character to get from A to B?") - - reduce hard disk i/o for server/client interaction (use fifos only?) - - further avoid exits that keep the server running and the lock file existing - when it shouldn't +- make server and client communicate by specific world state info requests + in server/out, replacing server/worldstate - check for return values of *printf() - make config files format more readable / self-explanatory +- get rid of the useless "internal error code" in error messages + SERVER: - implement field of view / line of sight and obstacles for those on the map diff --git a/roguelike b/roguelike index ae3fd75..3ea498d 100755 --- a/roguelike +++ b/roguelike @@ -10,20 +10,20 @@ set -e sleep 0.01 # The client should not start if the server is not running. (If the server was -# running in the foreround, any error exit of it so far would be caught by "set +# running in the foreground, any error exit of it so far would be caught by "set # -e" above. But "set -e" is blind to error codes generated in the background.) kill -0 $! 2> /dev/null -# Give server some time (max. 10 seconds) to generate its out file. +# Give server some time (max. 10 seconds) to generate its worldstate file. i=0 -while [ ! -e server/out ] && [ $i -le 1000 ] +while [ ! -e server/worldstate ] && [ $i -le 1000 ] do sleep 0.01 i=`expr $i + 1` done -if [ ! -e server/out ] +if [ ! -e server/worldstate ] then - echo "Server failed generating outfile within given time limit." + echo "Server failed generating worldstate file within given time limit." false fi diff --git a/src/client/cleanup.c b/src/client/cleanup.c index bcf52ac..f060250 100644 --- a/src/client/cleanup.c +++ b/src/client/cleanup.c @@ -4,6 +4,7 @@ #include /* for endwin() */ #include /* uint32_t */ #include /* free() */ +#include "../common/readwrite.h" /* try_fclose() */ #include "command_db.h" /* free_command_db() */ #include "misc.h" /* unload_interface_conf() */ #include "world.h" /* world global */ @@ -17,6 +18,7 @@ static uint32_t cleanup_flags = 0x0000; extern void cleanup() { + char * f_name = "cleanup()"; free(world.map.cells); free(world.log); free(world.player_inventory); @@ -32,6 +34,14 @@ extern void cleanup() { free_command_db(); } + if (cleanup_flags & CLEANUP_SERVER_IN) + { + try_fclose(world.file_server_in, f_name); + } + if (cleanup_flags & CLEANUP_SERVER_OUT) + { + try_fclose(world.file_server_out, f_name); + } } diff --git a/src/client/cleanup.h b/src/client/cleanup.h index ce4866d..c234c1c 100644 --- a/src/client/cleanup.h +++ b/src/client/cleanup.h @@ -16,7 +16,9 @@ enum cleanup_flag { CLEANUP_NCURSES = 0x0001, CLEANUP_INTERFACE = 0x0002, - CLEANUP_COMMANDS = 0x0004 + CLEANUP_COMMANDS = 0x0004, + CLEANUP_SERVER_IN = 0x0008, + CLEANUP_SERVER_OUT = 0x0008 }; extern void set_cleanup_flag(enum cleanup_flag flag); diff --git a/src/client/control.c b/src/client/control.c index f236ecf..745b2ab 100644 --- a/src/client/control.c +++ b/src/client/control.c @@ -4,7 +4,7 @@ #include /* uint8_t, uint16_t */ #include /* sprintf() */ #include /* strlen() */ -#include "io.h" /* try_send() */ +#include "io.h" /* send() */ #include "keybindings.h" /* get_command_to_keycode(), get_keycode_to_command(), * mod_selected_keyb(), move_keyb_selection() */ @@ -139,7 +139,7 @@ static uint8_t try_server_commands(struct Command * command) uint8_t arg_size = 3; char msg[command_size + 1 + arg_size + 1]; sprintf(msg, "%s %d", command->server_msg, arg); - try_send(msg); + send(msg); return 1; } return 0; diff --git a/src/client/io.c b/src/client/io.c index 27a0e0f..19dbb39 100644 --- a/src/client/io.c +++ b/src/client/io.c @@ -1,21 +1,21 @@ /* src/client/io.c */ #include "io.h" -#include /* global errno */ -#include /* open() */ #include /* PIPE_BUF */ #include /* halfdelay(), getch() */ #include /* NULL */ #include /* uint8_t, uint16_t, uint32_t */ -#include /* FILE, sprintf(), fseek() */ +#include /* FILE, sprintf(), fseek(), fflush() */ #include /* strcmp(), strlen(), memcpy() */ #include /* free(), atoi() */ #include /* stat() */ -#include /* access(), write() */ +#include /* time_t */ +#include /* time() */ +#include /* access() */ #include "../common/try_malloc.h" /* try_malloc() */ #include "../common/rexit.h" /* exit_trouble(), exit_err() */ #include "../common/readwrite.h" /* try_fopen(), try_fclose(), try_fgets(), - * try_fgetc(), textfile_width() + * try_fgetc(), textfile_width(), try_fputc() */ #include "control.h" /* try_key() */ #include "map.h" /* map_center() */ @@ -47,8 +47,8 @@ static void read_log(char * read_buf, uint32_t linemax, FILE * file); static uint16_t read_value_from_line(char * read_buf, uint32_t linemax, FILE * file); -/* If the server's out file has changed since the last read_world(), return a - * pointer to its file descriptor; else, return NULL. +/* If the server's worldstate file has changed since the last read_world(), + * return a pointer to its file descriptor; else, return NULL. * * Two tests are performed to check for a file change. The file's last data * modification time in seconds via stat() is compared against world.last_update @@ -61,17 +61,28 @@ static uint16_t read_value_from_line(char * read_buf, uint32_t linemax, * the new world also starts in turn 1, not signifying any world change to the * turn check. The stat() check detects this change with at most 1 second delay. */ -static FILE * changed_server_out_file(char * path); +static FILE * changed_worldstate_file(char * path); -/* Attempt to read the server's out file as representation of the game world in - * a hard-coded serialization format. Returns 1 on success and 0 if the out file - * wasn't read for supposedly not having changed since a last read_world() call. +/* Attempt to read the server's worldstate file as representation of the game + * world in a hard-coded serialization format. Returns 1 on success and 0 if the + * out file wasn't read for supposedly not having changed since a last + * read_world() call. * * map_center() is triggered by the first successful read_world() or on turn 1, * so the client focuses the map window on the player on client and world start. */ static uint8_t read_world(); +/* If "last_server_answer_time" is too old, send a PING to the server; or, if a + * previous PING has not sparked any answer after a while, abort the client. + */ +static void test_ping_pong(time_t last_server_answer_time); + +/* Update "last_server_answer_time" if new stuff has been written to the + * server's out file. + */ +static void test_server_activity(time_t * last_server_answer_time); + static void read_inventory(char * read_buf, uint32_t linemax, FILE * file) @@ -157,9 +168,9 @@ static uint16_t read_value_from_line(char * read_buf, uint32_t linemax, -static FILE * changed_server_out_file(char * path) +static FILE * changed_worldstate_file(char * path) { - char * f_name = "changed_server_out_file()"; + char * f_name = "changed_worldstate_file()"; struct stat stat_buf; exit_trouble(stat(path, &stat_buf), f_name, "stat()"); if (stat_buf.st_mtime != world.last_update) @@ -184,11 +195,11 @@ static FILE * changed_server_out_file(char * path) static uint8_t read_world() { char * f_name = "read_world()"; - char * path = "server/out"; - char * quit_msg = "No server out file found to read. Server may be down."; + char * path = "server/worldstate"; + char * quit_msg = "No worldstate file found to read. Server may be down."; static uint8_t first_read = 1; exit_err(access(path, F_OK), quit_msg); - FILE * file = changed_server_out_file(path); + FILE * file = changed_worldstate_file(path); if (!file) { return 0; @@ -216,37 +227,52 @@ static uint8_t read_world() -extern void try_send(char * msg) +static void test_ping_pong(time_t last_server_answer_time) { - char * f_name = "try_send()"; - uint32_t msg_size = strlen(msg) + 1; - char * err = "try_send() tries to send message larger than PIPE_BUF bytes."; - exit_err(msg_size > PIPE_BUF, err); - int fd_out; - uint16_t j = 1; - while (0 != j) + static uint8_t ping_sent = 0; + time_t now = time(0); + if (ping_sent && last_server_answer_time > now - 3) { - fd_out = open(world.path_server_in, O_WRONLY | O_NONBLOCK); - if (fd_out > 0) - { - break; - } - exit_err(-1 == fd_out && ENXIO != errno, "Server fifo not found."); - j++; + ping_sent = 0; } - exit_err(0 == j, "Failed to open server fifo for writing."); - j = 1; - while (0 != j) + if (!ping_sent && last_server_answer_time < now - 3) { - int test = write(fd_out, msg, msg_size); - if (test > 0) - { - break; - } - j++; + send("PING"); + ping_sent = 1; + return; + } + exit_err(last_server_answer_time < now - 6, "Server not answering."); +} + + + +static void test_server_activity(time_t * last_server_answer_time) +{ + char * f_name = "test_server_activity()"; + int test = try_fgetc(world.file_server_out, f_name); + if (EOF == test) + { + return; + } + do + { + ; } - exit_err(0 == j, "Failed to write to server fifo."); - exit_trouble(-1 == close(fd_out), f_name, "close()"); + while (EOF != (test = try_fgetc(world.file_server_out, f_name))); + * last_server_answer_time = time(0); +} + + + +extern void send(char * msg) +{ + char * f_name = "send()"; + uint32_t msg_size = strlen(msg) + 1; + char * err = "send() tries to send message larger than PIPE_BUF bytes."; + exit_err(msg_size > PIPE_BUF, err); + try_fwrite(msg, strlen(msg), 1, world.file_server_in, f_name); + try_fputc('\n', world.file_server_in, f_name); + fflush(world.file_server_in); } @@ -256,8 +282,11 @@ extern char * io_loop() world.halfdelay = 1; /* Ensures read_world() is only called 10 */ halfdelay(world.halfdelay); /* times a second during user inactivity. */ uint8_t change_in_client = 0; + time_t last_server_answer_time = time(0); while (1) { + test_server_activity(&last_server_answer_time); + test_ping_pong(last_server_answer_time); if (world.winch) { reset_windows_on_winch(); @@ -279,6 +308,6 @@ extern char * io_loop() } } } - try_send("QUIT"); + send("QUIT"); return "Sent QUIT to server."; } diff --git a/src/client/io.h b/src/client/io.h index 784c681..4db04cf 100644 --- a/src/client/io.h +++ b/src/client/io.h @@ -9,19 +9,23 @@ -/* Try sending "msg" to the server by writing it to the file at - * world.path_server_in. Try to open it 2^16 times before giving up. After - * opening, try to write to it 2^16 times before giving up. +/* Write "msg" plus newline to server input file at world.path_server_in. + * + * "msg" must fit into size defined by PIPE_BUF so that no race conditiosn + * arise by many clients writing to the file in parallel. */ -extern void try_send(char * msg); - -/* Keep checking for user input and a changed server out file. Update client's - * world representation on out file changes. Manipulate the client and send - * commands to server based on the user input as interpreted by the control.h - * library. On each change / activity, re-draw the windows with draw_all_wins(). - * When the loop ends regularly (due to the user sending a quit command), return - * an appropriate quit message to write to stdout when the client winds down. - * Also call reset_windows() on receiving a SIGWINCH. +extern void send(char * msg); + +/* Keep checking for user input, a changed worldstate file and the server's + * wakefulness. Update client's world representation on worldstate file changes. + * Manipulate the client and send commands to server based on the user input as + * interpreted by the control.h library. + * + * On each change / activity, re-draw the windows with draw_all_wins(). When the + * loop ends regularly (due to the user sending a quit command), return an + * appropriate quit message to write to stdout when the client winds down. Call + * reset_windows() on receiving a SIGWINCH. Abort on assumed server death if the + * server's out file does not get updated, even on PING requests. */ extern char * io_loop(); diff --git a/src/client/main.c b/src/client/main.c index c25c4f9..c021ef8 100644 --- a/src/client/main.c +++ b/src/client/main.c @@ -7,10 +7,11 @@ #include /* memset() */ #include /* access() */ #include "../common/err_try_fgets.h" /* set_err_try_fgets_delim() */ +#include "../common/readwrite.h" /* try_fopen() */ #include "../common/rexit.h" /* set_cleanup_func(), exit_trouble(),exit_err() */ #include "cleanup.h" /* cleanup(), set_cleanup_flag() */ #include "command_db.h" /* init_command_db() */ -#include "io.h" /* io_loop(), try_send() */ +#include "io.h" /* io_loop() */ #include "misc.h" /* load_interface_conf(), winch_called() */ #include "windows.h" /* winch_called() */ #include "world.h" /* struct World */ @@ -26,12 +27,13 @@ int main(int argc, char * argv[]) char * f_name = "main()"; /* Declare hard-coded paths and values here. */ - world.path_server_in = "server/in"; - world.path_commands = "confclient/commands"; - world.path_interface = "confclient/interface_conf"; - world.winDB.legal_ids = "012ciklm"; - world.delim = "%\n"; + world.path_commands = "confclient/commands"; + world.path_interface = "confclient/interface_conf"; + world.winDB.legal_ids = "012ciklm"; + world.delim = "%\n"; set_err_try_fgets_delim(world.delim); + char * path_server_in = "server/in"; + char * path_server_out = "server/out"; /* Parse command line arguments. */ obey_argv(argc, argv); @@ -58,6 +60,13 @@ int main(int argc, char * argv[]) act.sa_handler = &winch_called; exit_trouble(sigaction(SIGWINCH, &act, NULL), f_name, "sigaction()"); + /* Open file streams for communicating with the server. */ + exit_err(access(path_server_in, F_OK), "No server input file found."); + world.file_server_in = try_fopen(path_server_in, "a", f_name); + set_cleanup_flag(CLEANUP_SERVER_IN); + world.file_server_out = try_fopen(path_server_out, "r", f_name); + set_cleanup_flag(CLEANUP_SERVER_OUT); + /* This is where most everything happens. */ char * quit_msg = io_loop(); diff --git a/src/client/world.h b/src/client/world.h index 5b92f2e..d27a0a3 100644 --- a/src/client/world.h +++ b/src/client/world.h @@ -7,6 +7,7 @@ #define WORLD_H #include /* uint8_t, uint16_t */ +#include /* FILE */ #include /* time_t */ #include "map.h" /* struct Map */ #include "../common/yx_uint8.h" /* struct yx_uint8 */ @@ -18,15 +19,16 @@ struct World { + FILE * file_server_in; /* server input file to write commands to */ + FILE * file_server_out; /* server output file to read messages from */ struct WinDB winDB; /* data for window management and individual windows */ struct CommandDB commandDB; /* data on commands from commands config file */ struct KeyBindingDB kb_global; /* globally availabe keybindings */ struct KeyBindingDB kb_wingeom; /* Win geometry config view keybindings */ struct KeyBindingDB kb_winkeys; /* Win keybindings config view keybindings*/ struct Map map; /* game map geometry and content */ - time_t last_update; /* used for comparison with server outfile' mtime */ + time_t last_update; /* used for comparison with worldstate file's mtime */ char * log; /* log of player's activities */ - char * path_server_in; /* path of server's input fifo */ char * path_interface; /* path of interface configuration file */ char * path_commands; /* path of commands config file */ char * player_inventory; /* one-item-per-line string list of owned items */ diff --git a/src/server/cleanup.c b/src/server/cleanup.c index 4ad970c..08e3bb6 100644 --- a/src/server/cleanup.c +++ b/src/server/cleanup.c @@ -4,12 +4,14 @@ #include /* uint32_t */ #include /* free() */ #include /* unlink() */ +#include "../common/readwrite.h" /* try_fclose() */ #include "map_object_actions.h" /* free_map_object_actions() */ #include "map_objects.h" /* free_map_objects(), free_map_object_defs() */ #include "world.h" /* global world */ + /* The clean-up flags set by set_cleanup_flag(). */ static uint32_t cleanup_flags = 0x0000; @@ -17,12 +19,13 @@ static uint32_t cleanup_flags = 0x0000; extern void cleanup() { + char * f_name = "cleanup()"; free(world.queue); free(world.log); free(world.map.cells); - if (cleanup_flags & CLEANUP_OUTFILE) + if (cleanup_flags & CLEANUP_WORLDSTATE) { - unlink(world.path_out); + unlink(world.path_worldstate); } if (cleanup_flags & CLEANUP_MAP_OBJECTS) { @@ -36,10 +39,17 @@ extern void cleanup() { free_map_object_actions(world.map_obj_acts); } - if (cleanup_flags & CLEANUP_FIFO) /* Fifo also serves as lockfile that */ - { /* affirms the running of a server */ - unlink(world.path_in); /* instance. Therefore it should be */ - } /* the last thing to be deleted. */ + if (cleanup_flags & CLEANUP_IN) + { + try_fclose(world.file_in, f_name); + unlink(world.path_in); + } + if (cleanup_flags & CLEANUP_OUT) + { + try_fclose(world.file_out, f_name); + free(world.server_test); + unlink(world.path_out); + } } @@ -47,3 +57,10 @@ extern void set_cleanup_flag(enum cleanup_flag flag) { cleanup_flags = cleanup_flags | flag; } + + + +extern void unset_cleanup_flag(enum cleanup_flag flag) +{ + cleanup_flags = cleanup_flags ^ flag; +} diff --git a/src/server/cleanup.h b/src/server/cleanup.h index 61abd71..1652941 100644 --- a/src/server/cleanup.h +++ b/src/server/cleanup.h @@ -15,13 +15,17 @@ enum cleanup_flag { CLEANUP_FIFO = 0x0001, - CLEANUP_OUTFILE = 0x0002, + CLEANUP_WORLDSTATE = 0x0002, CLEANUP_MAP_OBJECT_DEFS = 0x0004, CLEANUP_MAP_OBJECTS = 0x0008, - CLEANUP_MAP_OBJECT_ACTS = 0x0010 + CLEANUP_MAP_OBJECT_ACTS = 0x0010, + CLEANUP_IN = 0x0020, + CLEANUP_OUT = 0x0040 }; +/* In addition, unset_cleanup_flag() may be used to unset flags. */ extern void set_cleanup_flag(enum cleanup_flag flag); +extern void unset_cleanup_flag(enum cleanup_flag flag); /* Frees memory and unlinks some files. */ extern void cleanup(); diff --git a/src/server/io.c b/src/server/io.c index 177c295..035f4fe 100644 --- a/src/server/io.c +++ b/src/server/io.c @@ -2,18 +2,17 @@ #include "io.h" #include /* global errno */ -#include /* open(), O_RDONLY, O_NONBLOCK */ #include /* PIPE_BUF */ #include /* size_t, NULL */ #include /* uint8_t, uint32_t */ -#include /* define FILE, sprintf() */ +#include /* defines EOF, FILE, sprintf() */ #include /* free() */ -#include /* strlen(), memset(), memcpy() */ -#include /* read(), close() */ +#include /* strlen(), memcpy() */ +#include /* time_t */ +#include /* time() */ #include "../common/readwrite.h" /* try_fopen(), try_fclose_unlink_rename(), - * try_fwrite(), try_fputc() + * try_fwrite(), try_fputc(), try_fgetc() */ -#include "../common/rexit.h" /* exit_trouble() */ #include "../common/try_malloc.h" /* try_malloc() */ #include "cleanup.h" /* set_cleanup_flag() */ #include "map_objects.h" /* structs MapObj, MapObjDef, get_map_obj_def() */ @@ -28,13 +27,15 @@ */ static char * get_message_from_queue(); -/* Read fifo input to put into queue. Only stop reading when bytes were received - * and the receiving has stopped. Read max. PIPE_BUF-sized chunks for atomicity. +/* Read input file for input into world.queue. new queue input. Wait a few + * seconds until giving up. Translate '\n' chars in input file into '\0' chars. */ -static void read_fifo_into_queue(); +static void read_file_into_queue(); -/* Write to output file the world state as it is to be visible to clients. */ -static void update_out_file(); +/* Write world state as visible to clients to its file. Write single dot line to + * server output file to satisfy client ping mechanisms. + */ +static void update_worldstate_file(); /* Write "value" to new \n-delimited line of "file". */ static void write_value_as_line(uint32_t value, FILE * file); @@ -53,76 +54,75 @@ static char * get_message_from_queue() { char * f_name = "get_message_from_queue()"; char * message = NULL; - size_t cutout_len = strlen(world.queue); - if (0 < cutout_len) - { - cutout_len++; - message = try_malloc(cutout_len, f_name); - memcpy(message, world.queue, cutout_len); - } - for (; - cutout_len != world.queue_size && '\0' == world.queue[cutout_len]; - cutout_len++); - world.queue_size = world.queue_size - cutout_len; - if (0 == world.queue_size) + if (world.queue_size) { - free(world.queue); /* NULL so read_fifo_into_queue() may free() this */ - world.queue = NULL; /* every time, even when it's un-allocated first. */ - } - else - { - char * new_queue = try_malloc(world.queue_size, f_name); - memcpy(new_queue, &(world.queue[cutout_len]), world.queue_size); - free(world.queue); - world.queue = new_queue; + size_t cutout_len = strlen(world.queue); + if (0 < cutout_len) + { + cutout_len++; + message = try_malloc(cutout_len, f_name); + memcpy(message, world.queue, cutout_len); + } + for (; + cutout_len != world.queue_size && '\0' == world.queue[cutout_len]; + cutout_len++); + world.queue_size = world.queue_size - cutout_len; + if (0 == world.queue_size) + { + free(world.queue); /* NULL so read_file_into_queue() may free() */ + world.queue = NULL; /* this every time, even when it's */ + } /* un-allocated first. */ + else + { + char * new_queue = try_malloc(world.queue_size, f_name); + memcpy(new_queue, &(world.queue[cutout_len]), world.queue_size); + free(world.queue); + world.queue = new_queue; + } } return message; } -static void read_fifo_into_queue() +static void read_file_into_queue() { - char * f_name = "read_fifo_into_queue()"; - uint32_t buf_size = PIPE_BUF; - int fdesc_infile = open(world.path_in, O_RDONLY | O_NONBLOCK); - exit_trouble(-1 == fdesc_infile, "open()", f_name); - char buf[buf_size]; - memset(buf, 0, buf_size); - int bytes_read; - uint8_t read_state = 0; /* 0: waiting for input. 1: started receiving it. */ - while (1) + char * f_name = "read_file_into_queue()"; + uint8_t wait_seconds = 5; + time_t now = time(0); + int test; + while (EOF == (test = try_fgetc(world.file_in, f_name))) { - bytes_read = read(fdesc_infile, buf, buf_size); - if (bytes_read > 0) + if (time(0) > now + wait_seconds) { - read_state = 1; - uint32_t old_queue_size = world.queue_size; - world.queue_size = world.queue_size + bytes_read; - char * new_queue = try_malloc(world.queue_size, f_name); - memcpy(new_queue, world.queue, old_queue_size); - memcpy(&(new_queue[old_queue_size]), buf, bytes_read); - free(world.queue); - world.queue = new_queue; - memset(buf, 0, buf_size); - continue; + return; } - exit_trouble(-1 == bytes_read && errno != EAGAIN, "read()", f_name); - if (1 == read_state) + } + do + { + char c = (char) test; + if ('\n' == c) { - break; + c = '\0'; } + char * new_queue = try_malloc(world.queue_size + 1, f_name); + memcpy(new_queue, world.queue, world.queue_size); + char * new_pos = new_queue + world.queue_size; + * new_pos = c; + world.queue_size++; + free(world.queue); + world.queue = new_queue; } - exit_trouble(close(fdesc_infile), f_name, "close()"); + while (EOF != (test = try_fgetc(world.file_in, f_name))); } -static void update_out_file() +static void update_worldstate_file() { - char * f_name = "update_out_file()"; - char path_tmp[strlen(world.path_out) + strlen(world.tmp_suffix) + 1]; - sprintf(path_tmp, "%s%s", world.path_out, world.tmp_suffix); + char * f_name = "update_worldstate_file()"; + char path_tmp[strlen(world.path_worldstate) + strlen(world.tmp_suffix) + 1]; + sprintf(path_tmp, "%s%s", world.path_worldstate, world.tmp_suffix); FILE * file = try_fopen(path_tmp, "w", f_name); struct MapObj * player = get_player(); write_value_as_line(world.turn, file); @@ -137,8 +137,11 @@ static void update_out_file() { try_fwrite(world.log, strlen(world.log), 1, file, f_name); } - try_fclose_unlink_rename(file, path_tmp, world.path_out, f_name); - set_cleanup_flag(CLEANUP_OUTFILE); + try_fclose_unlink_rename(file, path_tmp, world.path_worldstate, f_name); + set_cleanup_flag(CLEANUP_WORLDSTATE); + char * dot = ".\n";; + try_fwrite(dot, strlen(dot), 1, world.file_out, f_name); + fflush(world.file_out); } @@ -224,11 +227,11 @@ extern char * io_round() } if (world.turn != world.last_update_turn) { - update_out_file(); + update_worldstate_file(); world.last_update_turn = world.turn; } - read_fifo_into_queue(); - if ('\0' != world.queue[world.queue_size - 1]) + read_file_into_queue(); + if (world.queue_size && '\0' != world.queue[world.queue_size - 1]) { char * new_queue = try_malloc(world.queue_size + 1, f_name); memcpy(new_queue, world.queue, world.queue_size); diff --git a/src/server/io.h b/src/server/io.h index 78cb068..02f8e90 100644 --- a/src/server/io.h +++ b/src/server/io.h @@ -1,7 +1,7 @@ /* io.h: * - * Communication of the server with the outside world via input fifo and output - * file. + * Communication of the server with the outside world and its client via input, + * output and world state files. */ #ifndef IO_H @@ -11,8 +11,9 @@ /* Return single \0-terminated string read from input queue (world.queue); or, * if queue is empty and world.turn is unequal world.last_update_turn, update - * output file at world.path_out (and update world.last_update_turn), then read - * file at world.path_in for the next load of bytes to put onto the input queue. + * world state file at world.path_worldstate (and update world.last_update_turn + * and write a single dot line to output file at world.path_out), then read file + * at world.path_in for the next load of bytes to put onto the input queue. * * Reading the file at world.path_in may put many \0-terminated strings on the * queue at once. Successive calls of io_round() will make these available one diff --git a/src/server/main.c b/src/server/main.c index 64d4434..769d47c 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -1,12 +1,17 @@ /* src/server/main.c */ #include /* global errno */ -#include /* printf() */ +#include /* printf(), fflush() */ #include /* exit() */ +#include /* strlen() */ #include /* mkfifo(), mkdir() */ -#include /* access() */ +#include /* defines pid_t, time_t */ +#include /* #time() */ +#include /* access(), getpid(), unlink() */ #include "../common/err_try_fgets.h" /* set_err_try_fgets_delim() */ -#include "../common/rexit.h" /* exit_err, exit_trouble(), set_cleanup_func() */ +#include "../common/readwrite.h" /* try_fopen(), try_fwrite(), try_fclose() */ +#include "../common/rexit.h" /* exit_err, set_cleanup_func() */ +#include "../common/try_malloc.h" /* try_malloc() */ #include "cleanup.h" /* set_cleanup_flag(), cleanup() */ #include "init.h" /* run_game(), obey_argv() */ #include "map_object_actions.h" /* init_map_object_actions() */ @@ -43,10 +48,11 @@ int main(int argc, char ** argv) } world.path_map_obj_defs = "confserver/defs"; world.path_map_obj_acts = "confserver/map_object_actions"; - world.path_in = "server/in"; - world.path_out = "server/out"; - world.path_record = "record"; - world.tmp_suffix = "_tmp"; + world.path_worldstate = "server/worldstate"; + world.path_out = "server/out"; + world.path_in = "server/in"; + world.path_record = "record"; + world.tmp_suffix = "_tmp"; set_err_try_fgets_delim("%%\n"); /* Set map geometry. */ @@ -61,19 +67,33 @@ int main(int argc, char ** argv) exit_err(access(world.path_map_obj_defs, F_OK), err_mod); exit_err(access(world.path_map_obj_acts, F_OK), err_moa); - /* Treat world.path_in file as server process lock file. */ - char * err = "Found pre-existing input fifo file. This indicates another " - "roguelike-server may be running. It should be killed first."; - exit_err(!access(world.path_in, F_OK), err); - int test = mkdir("server", 0700); - exit_trouble(test && EEXIST != errno, f_name, "mkdir()"); - exit_trouble(mkfifo(world.path_in, 0600), f_name, "mkfifo()"); - set_cleanup_flag(CLEANUP_FIFO); - /* Init from config files map object (action) definitions. */ init_map_object_defs(); init_map_object_actions(); + /* Create server directory if it does not exist yet. */ + int test = mkdir("server", 0700); + exit_trouble(test && EEXIST != errno, f_name, "mkdir()"); + + /* Create server out file and start it with server process test string. */ + world.file_out = try_fopen(world.path_out, "w", f_name); + world.server_test = try_malloc(10 + 1 + 10 + 1 + 1, f_name); + sprintf(world.server_test, "%d %d\n", getpid(), (int) time(0)); + try_fwrite(world.server_test, strlen(world.server_test), 1, + world.file_out, f_name); + fflush(world.file_out); + set_cleanup_flag(CLEANUP_OUT); + + /* Create server in file, switch to reading it. */ + if (!access(world.path_in, F_OK)) /* This keeps out input from old input */ + { /* file streams of clients */ + unlink(world.path_in); /* communicating with server processes */ + } /* superseded by this current one. */ + world.file_in = try_fopen(world.path_in, "w", f_name); + try_fclose(world.file_in, f_name); + world.file_in = try_fopen(world.path_in, "r", f_name); + set_cleanup_flag(CLEANUP_IN); + /* Enter play or replay mode loops, then leave properly. */ run_game(); cleanup(); diff --git a/src/server/run.c b/src/server/run.c index 82e0aae..f5d0e17 100644 --- a/src/server/run.c +++ b/src/server/run.c @@ -3,16 +3,17 @@ #include "run.h" #include /* NULL */ #include /* uint8_t, uint16_t, uint32_t */ -#include /* FILE, sprintf() */ +#include /* FILE, sprintf(), fflush() */ #include /* free() */ -#include /* strlen(), strncmp(), atoi() */ +#include /* strlen(), strcmp() strncmp(), atoi() */ #include /* access() */ #include "../common/readwrite.h" /* try_fopen(), try_fcose(), try_fwrite(), * try_fgets(), try_fclose_unlink_rename(), * textfile_width(), try_fputc() */ -#include "../common/rexit.h" /* exit_trouble() */ +#include "../common/rexit.h" /* exit_trouble(), exit_err() */ #include "ai.h" /* ai() */ +#include "cleanup.h" /* unset_cleanup_flag() */ #include "init.h" /* remake_world() */ #include "io.h" /* io_round() */ #include "map_object_actions.h" /* get_moa_id_by_name() */ @@ -44,6 +45,13 @@ static uint8_t is_effort_finished(struct MapObjAct * moa, */ static uint8_t apply_player_command(char * msg, char * command_name); +/* Compares first line of file at world.path_out to world.server_test, aborts if + * they don't match, but not before unsetting the flags deleting files in the + * server directory, for in that case those must be assumed to belong to another + * server process. + */ +static void server_test(); + static void turn_over() @@ -134,6 +142,27 @@ static uint8_t apply_player_command(char * msg, char * command_name) +static void server_test() +{ + char * f_name = "server_test()"; + char test[10 + 1 + 10 + 1 + 1]; + FILE * file = try_fopen(world.path_out, "r", f_name); + try_fgets(test, 10 + 10 + 1 + 1, file, f_name); + try_fclose(file, f_name); + if (strcmp(test, world.server_test)) + { + unset_cleanup_flag(CLEANUP_WORLDSTATE); + unset_cleanup_flag(CLEANUP_OUT); + unset_cleanup_flag(CLEANUP_IN); + char * msg = "Server test string in server output file does not match. " + "This indicates that the current server process has been " + "superseded by another one."; + exit_err(1, msg); + } +} + + + extern void obey_msg(char * msg, uint8_t do_record) { char * f_name = "obey_msg()"; @@ -179,6 +208,7 @@ extern uint8_t io_loop() char * f_name = "io_loop()"; while (1) { + server_test(); char * msg = io_round(); if (NULL == msg) { @@ -193,6 +223,14 @@ extern uint8_t io_loop() free(msg); return 1; } + if (!strcmp("PING", msg)) + { + free(msg); + char * pong = "PONG\n"; + try_fwrite(pong, strlen(pong), 1, world.file_out, f_name); + fflush(world.file_out); + continue; + } if (world.replay) { free(msg); diff --git a/src/server/run.h b/src/server/run.h index 877d2fb..9fa526f 100644 --- a/src/server/run.h +++ b/src/server/run.h @@ -17,7 +17,10 @@ extern void obey_msg(char * msg, uint8_t do_record); /* Loop for receiving commands via io_round() and acting on them. Exits with 1 * on "QUIT" command. In replay mode, exits with 0 on each non-"QUIT" command. - * In play mode, processes incomming commands via obey_msg(). + * Writes a "PONG" line to server output file on "PING" command. In play mode, + * processes further incomming commands via obey_msg(). Compares the first line + * of the file at world.path_out with world.server_test to ensure that the + * current server process has not been superseded by a new one. */ extern uint8_t io_loop(); diff --git a/src/server/world.h b/src/server/world.h index 84235ce..47f7c65 100644 --- a/src/server/world.h +++ b/src/server/world.h @@ -7,6 +7,7 @@ #define MAIN_H #include /* uint8_t, uint16_t, uint32_t */ +#include /* define FILE */ #include "map.h" /* struct Map */ struct MapObjDef; struct MapObjAct; @@ -16,18 +17,22 @@ struct MapObj; struct World { + FILE * file_in; /* Input stream on file at .path_in. */ + FILE * file_out; /* Output stream on file at .path_out. */ struct Map map; struct MapObjDef * map_obj_defs; /* Map object definitions. */ struct MapObjAct * map_obj_acts; /* Map object action definitions. */ struct MapObj * map_objs; /* Map objects. */ char * log; /* Logs the game events from the player's view. */ - char * path_in; /* Fifo to receive command messages. */ - char * path_out; /* File to write the game state as visible to clients.*/ + char * server_test; /* String uniquely identifying server process. */ + char * path_in; /* File to write client messages into. */ + char * path_out; /* File to write server messages into. */ + char * path_worldstate; /* File to represent world state to clients.*/ char * path_record; /* Record file from which to read the game history. */ char * path_map_obj_defs; /* path for map object definitions config file */ char * path_map_obj_acts; /* path for map object actions config file */ char * tmp_suffix; /* Appended to paths of files for their tmp versions. */ - char * queue; /* Stores un-processed messages received via input fifo. */ + char * queue; /* Stores un-processed messages read from the input file. */ uint32_t queue_size;/* Length of .queue sequence of \0-terminated strings.*/ uint32_t seed; /* Randomness seed. */ uint16_t replay; /* Turn up to which to replay game. No replay if zero. */