summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--crawl-ref/source/AppHdr.h2
-rw-r--r--crawl-ref/source/Makefile2
-rw-r--r--crawl-ref/source/Makefile.obj1
-rw-r--r--crawl-ref/source/cio.cc8
-rw-r--r--crawl-ref/source/colour.cc2
-rw-r--r--crawl-ref/source/directn.cc51
-rw-r--r--crawl-ref/source/directn.h2
-rw-r--r--crawl-ref/source/godabil.cc15
-rw-r--r--crawl-ref/source/initfile.cc23
-rw-r--r--crawl-ref/source/libunix.cc60
-rw-r--r--crawl-ref/source/libunix.h2
-rw-r--r--crawl-ref/source/libutil.cc10
-rw-r--r--crawl-ref/source/libweb.cc214
-rw-r--r--crawl-ref/source/main.cc16
-rw-r--r--crawl-ref/source/output.cc16
-rw-r--r--crawl-ref/source/state.cc2
-rw-r--r--crawl-ref/source/state.h2
-rw-r--r--crawl-ref/source/tileweb.cc422
-rw-r--r--crawl-ref/source/tileweb.h62
-rw-r--r--crawl-ref/source/view.cc27
-rw-r--r--crawl-ref/source/view.h4
-rw-r--r--crawl-ref/source/viewgeom.cc6
-rw-r--r--crawl-ref/source/viewmap.cc31
-rw-r--r--crawl-ref/source/webserver/config.py6
-rw-r--r--crawl-ref/source/webserver/connection.py81
-rw-r--r--crawl-ref/source/webserver/process_handler.py219
-rwxr-xr-xcrawl-ref/source/webserver/server.py364
-rw-r--r--crawl-ref/source/webserver/static/client.js41
-rw-r--r--crawl-ref/source/webserver/static/game/game.js11
-rw-r--r--crawl-ref/source/webserver/templates/lobby.html6
-rw-r--r--crawl-ref/source/webserver/terminal.py127
31 files changed, 1084 insertions, 751 deletions
diff --git a/crawl-ref/source/AppHdr.h b/crawl-ref/source/AppHdr.h
index eaa1fa23a6..976e3fc121 100644
--- a/crawl-ref/source/AppHdr.h
+++ b/crawl-ref/source/AppHdr.h
@@ -96,7 +96,7 @@
#endif
#define FILE_SEPARATOR '/'
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
#define USE_CURSES
#endif
diff --git a/crawl-ref/source/Makefile b/crawl-ref/source/Makefile
index 2ee432fc65..fff221bf8d 100644
--- a/crawl-ref/source/Makefile
+++ b/crawl-ref/source/Makefile
@@ -319,7 +319,7 @@ ifdef WIN32
EXTRA_OBJECTS += icon.o
endif
-ifndef TILES_ANY
+ifndef TILES
ifdef NEED_LIBW32C
OBJECTS += libw32c.o
else
diff --git a/crawl-ref/source/Makefile.obj b/crawl-ref/source/Makefile.obj
index 76ff5b07aa..f1ec05c8b9 100644
--- a/crawl-ref/source/Makefile.obj
+++ b/crawl-ref/source/Makefile.obj
@@ -268,7 +268,6 @@ endif
ifdef WEBTILES
OBJECTS += \
-libweb.o \
tiledoll.o \
tilemcache.o \
tilepick.o \
diff --git a/crawl-ref/source/cio.cc b/crawl-ref/source/cio.cc
index 8606113fe7..c5aff06e01 100644
--- a/crawl-ref/source/cio.cc
+++ b/crawl-ref/source/cio.cc
@@ -21,7 +21,7 @@ extern int unixcurses_get_vi_key(int keyin);
static keycode_type _numpad2vi(keycode_type key)
{
-#if defined(UNIX) && !defined(USE_TILE)
+#if defined(UNIX) && !defined(USE_TILE_LOCAL)
key = unixcurses_get_vi_key(key);
#endif
switch (key)
@@ -30,7 +30,7 @@ static keycode_type _numpad2vi(keycode_type key)
case CK_DOWN: key = 'j'; break;
case CK_LEFT: key = 'h'; break;
case CK_RIGHT: key = 'l'; break;
-#if defined(UNIX) && !defined(USE_TILE)
+#if defined(UNIX) && !defined(USE_TILE_LOCAL)
case -1001: key = 'b'; break;
case -1002: key = 'j'; break;
case -1003: key = 'n'; break;
@@ -84,7 +84,7 @@ int unmangle_direction_keys(int keyin, KeymapContext keymap,
case '8': return 'k';
case '9': return 'u';
- #ifndef USE_TILE
+ #ifndef USE_TILE_LOCAL
default: return unixcurses_get_vi_key(keyin);
#endif
@@ -107,7 +107,7 @@ int unmangle_direction_keys(int keyin, KeymapContext keymap,
// cursoring over darkgrey or black causes problems.
void cursorxy(int x, int y)
{
-#if defined(USE_TILE)
+#if defined(USE_TILE_LOCAL)
coord_def ep(x, y);
coord_def gc = crawl_view.screen2grid(ep);
tiles.place_cursor(CURSOR_MOUSE, gc);
diff --git a/crawl-ref/source/colour.cc b/crawl-ref/source/colour.cc
index c95758b061..8bb36fcfa3 100644
--- a/crawl-ref/source/colour.cc
+++ b/crawl-ref/source/colour.cc
@@ -802,7 +802,7 @@ unsigned real_colour(unsigned raw_colour, const coord_def& loc)
if (_is_element_colour(raw_colour))
raw_colour = colflags | element_colour(raw_colour, false, loc);
-#if defined(TARGET_OS_WINDOWS) || defined(USE_TILE)
+#if defined(TARGET_OS_WINDOWS) || defined(USE_TILE_LOCAL)
if (colflags)
{
unsigned brand = _colflag2brand(colflags);
diff --git a/crawl-ref/source/directn.cc b/crawl-ref/source/directn.cc
index d979e93648..b839e2971a 100644
--- a/crawl-ref/source/directn.cc
+++ b/crawl-ref/source/directn.cc
@@ -114,7 +114,7 @@ static bool _find_feature(const coord_def& where, int mode, bool need_path,
static bool _find_fprop_unoccupied(const coord_def& where, int mode, bool need_path,
int range, targetter *hitfunc);
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
static bool _find_mlist(const coord_def& where, int mode, bool need_path,
int range, targetter *hitfunc);
#endif
@@ -424,7 +424,7 @@ void direction_chooser::describe_cell() const
flush_prev_message();
}
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
static void _draw_ray_glyph(const coord_def &pos, int colour,
int glych, int mcol)
{
@@ -657,7 +657,7 @@ void full_describe_view()
// (A) An angel (neutral), wielding a glowing long sword
std::string prefix = "";
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
glyph g = get_mons_glyph(mi->mon());
const std::string col_string = colour_to_str(g.col);
prefix = "(<" + col_string + ">"
@@ -673,7 +673,7 @@ void full_describe_view()
if (mi->dam != MDAM_OKAY)
str += ", " + mi->damage_desc();
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
// Wraparound if the description is longer than allowed.
linebreak_string(str, get_number_of_cols() - 9);
#endif
@@ -684,7 +684,7 @@ void full_describe_view()
{
if (j == 0)
me = new MonsterMenuEntry(prefix+str, mi->mon(), hotkey++);
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
else
{
str = " " + fss[j].tostring();
@@ -710,7 +710,7 @@ void full_describe_view()
for (unsigned int i = 0; i < all_items.size(); ++i, hotkey++)
{
InvEntry *me = all_items[i];
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
// Show glyphs only for ASCII.
me->set_show_glyph(true);
#endif
@@ -729,7 +729,7 @@ void full_describe_view()
{
const coord_def c = list_features[i];
std::string desc = "";
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
glyph g = get_cell_glyph(c);
const std::string colour_str = colour_to_str(g.col);
desc = "(<" + colour_str + ">";
@@ -825,7 +825,7 @@ void full_describe_view()
}
}
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
if (!list_items.empty())
{
// Unset show_glyph for other menus.
@@ -833,7 +833,8 @@ void full_describe_view()
me->set_show_glyph(false);
delete me;
}
-#else
+#endif
+#ifdef USE_TILE
// Clear cursor placement.
tiles.place_cursor(CURSOR_TUTORIAL, NO_CURSOR);
tiles.clear_text_tags(TAG_TUTORIAL);
@@ -862,7 +863,7 @@ void full_describe_view()
//
//--------------------------------------------------------------
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
// XXX: Hack - can't pass mlist entries into _find_mlist().
bool mlist_full_info;
std::vector<monster_info> mlist;
@@ -1041,7 +1042,7 @@ static std::string _targ_mode_name(targ_mode_type mode)
}
}
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
static void _update_mlist(bool enable)
{
crawl_state.mlist_targeting = enable;
@@ -1163,7 +1164,7 @@ void direction_chooser::draw_beam_if_needed()
if (!show_beam)
{
viewwindow(
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
false
#endif
);
@@ -1179,7 +1180,7 @@ void direction_chooser::draw_beam_if_needed()
if (!hitfunc->valid_aim(target()))
{
#ifdef USE_TILE
- viewwindow();
+ viewwindow(false, true);
#endif
return;
}
@@ -1190,7 +1191,8 @@ void direction_chooser::draw_beam_if_needed()
{
#ifdef USE_TILE
tile_place_ray(*ri, aff);
-#else
+#endif
+#ifndef USE_TILE_LOCAL
int bcol = BLACK;
if (aff < 0)
bcol = DARKGREY;
@@ -1202,7 +1204,7 @@ void direction_chooser::draw_beam_if_needed()
#endif
}
#ifdef USE_TILE
- viewwindow();
+ viewwindow(false, true);
#endif
return;
}
@@ -1212,7 +1214,7 @@ void direction_chooser::draw_beam_if_needed()
{
#ifdef USE_TILE
// Clear the old beam if we're not drawing anything else.
- viewwindow();
+ viewwindow(false, true);
#endif
return;
}
@@ -1236,7 +1238,8 @@ void direction_chooser::draw_beam_if_needed()
const bool inrange = in_range(p);
#ifdef USE_TILE
tile_place_ray(p, inrange ? AFF_YES : AFF_NO);
-#else
+#endif
+#ifndef USE_TILE_LOCAL
const int bcol = inrange ? MAGENTA : DARKGREY;
_draw_ray_glyph(p, bcol, '*', bcol | COLFLAG_REVERSE);
#endif
@@ -1246,7 +1249,7 @@ void direction_chooser::draw_beam_if_needed()
tile_place_ray(target(), in_range(ray.pos()) ? AFF_YES : AFF_NO);
// In tiles, we need to refresh the window to get the beam drawn.
- viewwindow();
+ viewwindow(false, true);
#endif
}
@@ -1837,7 +1840,7 @@ command_type direction_chooser::massage_command(command_type key_command) const
void direction_chooser::handle_mlist_cycle_command(command_type key_command)
{
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
if (key_command >= CMD_TARGET_CYCLE_MLIST
&& key_command <= CMD_TARGET_CYCLE_MLIST_END)
{
@@ -1904,7 +1907,7 @@ bool direction_chooser::do_main_loop()
{
case CMD_TARGET_SHOW_PROMPT: describe_cell(); break;
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
case CMD_TARGET_TOGGLE_MLIST:
Options.mlist_targeting = !Options.mlist_targeting;
_update_mlist(Options.mlist_targeting);
@@ -2053,7 +2056,7 @@ void direction_chooser::finalize_moves()
bool direction_chooser::choose_direction()
{
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
if (may_target_monster && restricts != DIR_DIR && Options.mlist_targeting)
_update_mlist(true);
#endif
@@ -2100,7 +2103,7 @@ bool direction_chooser::choose_direction()
;
msgwin_set_temporary(false);
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
_update_mlist(false);
#endif
finalize_moves();
@@ -2314,7 +2317,7 @@ static bool _mons_is_valid_target(const monster* mon, int mode, int range)
return (true);
}
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
static bool _find_mlist(const coord_def& where, int idx, bool need_path,
int range, targetter *hitfunc)
{
@@ -4033,7 +4036,7 @@ command_type targeting_behaviour::get_command(int key)
if (cmd >= CMD_MIN_TARGET && cmd < CMD_TARGET_CYCLE_TARGET_MODE)
return (cmd);
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
// Overrides the movement keys while mlist_targeting is active.
if (crawl_state.mlist_targeting && isalower(key))
return static_cast<command_type>(CMD_TARGET_CYCLE_MLIST + (key - 'a'));
diff --git a/crawl-ref/source/directn.h b/crawl-ref/source/directn.h
index 33f6c06eb5..4dc350b77a 100644
--- a/crawl-ref/source/directn.h
+++ b/crawl-ref/source/directn.h
@@ -272,7 +272,7 @@ enum mons_equip_desc_level_type
DESC_IDENTIFIED,
};
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
char mlist_index_to_letter(int index);
#endif
diff --git a/crawl-ref/source/godabil.cc b/crawl-ref/source/godabil.cc
index fbb978deff..dffa98e8a5 100644
--- a/crawl-ref/source/godabil.cc
+++ b/crawl-ref/source/godabil.cc
@@ -2482,11 +2482,12 @@ bool fedhas_plant_ring_from_fruit()
for (int i = 0; i < max_use; ++i)
{
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
coord_def temp = grid2view(adjacent[i]);
cgotoxy(temp.x, temp.y, GOTO_DNGN);
put_colour_ch(GREEN, '1' + i);
-#else
+#endif
+#ifdef USE_TILE
tiles.add_overlay(adjacent[i], TILE_INDICATOR + i);
#endif
}
@@ -2504,7 +2505,7 @@ bool fedhas_plant_ring_from_fruit()
// The user entered a number, remove all number overlays which
// are higher than that number.
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
unsigned not_used = adjacent.size() - unsigned(target_count);
for (unsigned i = adjacent.size() - not_used;
i < adjacent.size();
@@ -2512,7 +2513,8 @@ bool fedhas_plant_ring_from_fruit()
{
view_update_at(adjacent[i]);
}
-#else
+#endif
+#ifdef USE_TILE
// For tiles we have to clear all overlays and redraw the ones
// we want.
tiles.clear_overlays();
@@ -2680,7 +2682,7 @@ int fedhas_corpse_spores(beh_type behavior, bool interactive)
viewwindow(false);
for (unsigned i = 0; i < positions.size(); ++i)
{
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
coord_def temp = grid2view(positions[i]->pos);
cgotoxy(temp.x, temp.y, GOTO_DNGN);
@@ -2690,7 +2692,8 @@ int fedhas_corpse_spores(beh_type behavior, bool interactive)
unsigned character = mons_char(MONS_GIANT_SPORE);
put_colour_ch(color, character);
-#else
+#endif
+#ifdef USE_TILE
tiles.add_overlay(positions[i]->pos, TILE_SPORE_OVERLAY);
#endif
}
diff --git a/crawl-ref/source/initfile.cc b/crawl-ref/source/initfile.cc
index 6cdeea87e8..efa356a9cc 100644
--- a/crawl-ref/source/initfile.cc
+++ b/crawl-ref/source/initfile.cc
@@ -26,6 +26,9 @@
#ifdef USE_TILE
#include "tilereg-map.h"
#endif
+#ifdef USE_TILE_WEB
+ #include "tileweb.h"
+#endif
#include "invent.h"
#include "item_use.h"
#include "libutil.h"
@@ -3417,6 +3420,10 @@ enum commandline_option_type
CLO_ZOTDEF,
CLO_TUTORIAL,
CLO_WIZARD,
+#ifdef USE_TILE_WEB
+ CLO_WEBTILES_SOCKET,
+ CLO_AWAIT_CONNECTION,
+#endif
CLO_NOPS
};
@@ -3427,7 +3434,10 @@ static const char *cmd_ops[] = {
"mapstat", "arena", "dump-maps", "test", "script", "builddb",
"help", "version", "seed", "save-version", "sprint",
"extra-opt-first", "extra-opt-last", "sprint-map", "edit-save",
- "print-charset", "zotdef", "tutorial", "wizard"
+ "print-charset", "zotdef", "tutorial", "wizard",
+#ifdef USE_TILE_WEB
+ "webtiles-socket", "await-connection",
+#endif
};
static const int num_cmd_ops = CLO_NOPS;
@@ -4068,6 +4078,17 @@ bool parse_args(int argc, char **argv, bool rc_only)
#endif
break;
+#ifdef USE_TILE_WEB
+ case CLO_WEBTILES_SOCKET:
+ nextUsed = true;
+ tiles.m_sock_name = next_arg;
+ break;
+
+ case CLO_AWAIT_CONNECTION:
+ tiles.m_await_connection = true;
+ break;
+#endif
+
case CLO_PRINT_CHARSET:
if (rc_only)
break;
diff --git a/crawl-ref/source/libunix.cc b/crawl-ref/source/libunix.cc
index 80143870bf..deaee2afac 100644
--- a/crawl-ref/source/libunix.cc
+++ b/crawl-ref/source/libunix.cc
@@ -227,6 +227,17 @@ int getchk()
}
wint_t c;
+
+#ifdef USE_TILE_WEB
+ refresh();
+
+ tiles.redraw();
+ tiles.await_input(c, true);
+
+ if (c > 0)
+ return c;
+#endif
+
switch (get_wch(&c))
{
case ERR:
@@ -414,10 +425,18 @@ void console_startup(void)
crawl_view.init_geometry();
set_mouse_enabled(false);
+
+#ifdef USE_TILE_WEB
+ tiles.resize();
+#endif
}
void console_shutdown()
{
+#ifdef USE_TILE_WEB
+ tiles.shutdown();
+#endif
+
// resetty();
endwin();
@@ -457,6 +476,13 @@ void putwch(ucs_t chr)
c = ' ';
// TODO: recognize unsupported characters and try to transliterate
addnwstr(&c, 1);
+
+#ifdef USE_TILE_WEB
+ ucs_t buf[2];
+ buf[0] = chr;
+ buf[1] = 0;
+ tiles.put_ucs_string(buf);
+#endif
}
void puttext(int x1, int y1, const crawl_view_buffer &vbuf)
@@ -483,6 +509,10 @@ void puttext(int x1, int y1, const crawl_view_buffer &vbuf)
void update_screen(void)
{
refresh();
+
+#ifdef USE_TILE_WEB
+ tiles.set_need_redraw();
+#endif
}
void clear_to_end_of_line(void)
@@ -490,6 +520,10 @@ void clear_to_end_of_line(void)
textcolor(LIGHTGREY);
textbackground(BLACK);
clrtoeol();
+
+#ifdef USE_TILE_WEB
+ tiles.clear_to_end_of_line();
+#endif
}
int get_number_of_lines(void)
@@ -511,6 +545,10 @@ void clrscr()
printf("%s", DGL_CLEAR_SCREEN);
fflush(stdout);
#endif
+
+#ifdef USE_TILE_WEB
+ tiles.clrscr();
+#endif
}
void set_cursor_enabled(bool enabled)
@@ -604,6 +642,10 @@ static int curs_fg_attr(int col)
void textcolor(int col)
{
(void)attrset(Current_Colour = curs_fg_attr(col));
+
+#ifdef USE_TILE_WEB
+ tiles.textcolor(col);
+#endif
}
static int curs_bg_attr(int col)
@@ -662,6 +704,10 @@ static int curs_bg_attr(int col)
void textbackground(int col)
{
(void)attrset(Current_Colour = curs_bg_attr(col));
+
+#ifdef USE_TILE_WEB
+ tiles.textbackground(col);
+#endif
}
@@ -745,6 +791,11 @@ int wherey()
void delay(unsigned int time)
{
+#ifdef USE_TILE_WEB
+ tiles.redraw();
+ tiles.send_message("delay(%d);", time);
+#endif
+
refresh();
if (time)
usleep(time * 1000);
@@ -757,6 +808,7 @@ bool kbhit()
return true;
wint_t c;
+#ifndef USE_TILE_WEB
int i;
nodelay(stdscr, TRUE);
@@ -775,4 +827,12 @@ bool kbhit()
default:
return false;
}
+#else
+ bool result = tiles.await_input(c, false);
+
+ if (result && (c != 0))
+ pending = c;
+
+ return result;
+#endif
}
diff --git a/crawl-ref/source/libunix.h b/crawl-ref/source/libunix.h
index 5f971d41dc..83f20697d5 100644
--- a/crawl-ref/source/libunix.h
+++ b/crawl-ref/source/libunix.h
@@ -5,7 +5,7 @@
#define O_BINARY 0
#endif
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
#include <stdio.h>
diff --git a/crawl-ref/source/libutil.cc b/crawl-ref/source/libutil.cc
index 5c5211a98e..ddeddd8cad 100644
--- a/crawl-ref/source/libutil.cc
+++ b/crawl-ref/source/libutil.cc
@@ -905,7 +905,7 @@ bool version_is_stable(const char *v)
}
}
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
coord_def cgettopleft(GotoRegion region)
{
switch (region)
@@ -942,13 +942,17 @@ void cgotoxy(int x, int y, GotoRegion region)
ASSERT_SAVE(y >= 1 && y <= sz.y);
gotoxy_sys(tl.x + x - 1, tl.y + y - 1);
+
+#ifdef USE_TILE_WEB
+ tiles.cgotoxy(x, y, region);
+#endif
}
GotoRegion get_cursor_region()
{
return (_current_region);
}
-#endif // USE_TILE
+#endif // USE_TILE_LOCAL
coord_def cgetsize(GotoRegion region)
{
@@ -1097,7 +1101,7 @@ static void handle_hangup(int)
if (crawl_state.seen_hups++)
return;
-#ifdef USE_TILE
+#ifdef USE_TILE_LOCAL
// XXX: Will a tiles build ever need to handle the HUP signal?
sighup_save_and_exit();
#elif defined(USE_CURSES)
diff --git a/crawl-ref/source/libweb.cc b/crawl-ref/source/libweb.cc
deleted file mode 100644
index 44478e87c6..0000000000
--- a/crawl-ref/source/libweb.cc
+++ /dev/null
@@ -1,214 +0,0 @@
-/**
- * @file
- * @brief Functions that any display port needs to implement -- webtiles version.
-**/
-
-#include "AppHdr.h"
-
-#ifdef USE_TILE_WEB
-
-#include <unistd.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-
-#include "cio.h"
-#include "defines.h"
-#include "env.h"
-#include "externs.h"
-#include "message.h"
-#include "stash.h"
-#include "state.h"
-#include "stuff.h"
-#include "terrain.h"
-#include "tiledef-main.h"
-#include "travel.h"
-#include "viewgeom.h"
-
-int m_getch()
-{
- return getchk();
-}
-
-void set_mouse_enabled(bool enabled)
-{
- crawl_state.mouse_enabled = enabled;
-}
-
-void gui_init_view_params(crawl_view_geometry &geom)
-{
- // The tile version handles its own layout on a pixel-by-pixel basis.
- // Pretend that all of the regions start at character location (1,1).
-
- geom.termp.x = 1;
- geom.termp.y = 1;
-
- geom.termsz.x = 80;
- geom.termsz.y = 24;
-
- geom.viewp.x = 1;
- geom.viewp.y = 1;
-
- geom.hudp.x = 1;
- geom.hudp.y = 1;
-
- geom.msgp.x = 1;
- geom.msgp.y = 1;
-
- geom.mlistp.x = 1;
- geom.mlistp.y = 1;
- geom.mlistsz.x = 0;
- geom.mlistsz.y = 0;
-
- geom.viewsz.x = 17;
- geom.viewsz.y = 17;
-}
-
-void putwch(ucs_t chr)
-{
- if (!chr)
- chr = ' ';
- ucs_t buf[2];
- buf[0] = chr;
- buf[1] = 0;
- tiles.put_ucs_string(buf);
-}
-
-void clear_to_end_of_line()
-{
- tiles.clear_to_end_of_line();
-}
-
-void cprintf(const char *format,...)
-{
- char buffer[2048]; // One full screen if no control seq...
- va_list argp;
- va_start(argp, format);
- vsnprintf(buffer, sizeof(buffer), format, argp);
- va_end(argp);
- tiles.put_string(buffer);
-}
-
-void textcolor(int color)
-{
- tiles.textcolor(color);
-}
-
-void textbackground(int bg)
-{
- tiles.textbackground(bg);
-}
-
-void set_cursor_enabled(bool enabled)
-{
- // if (enabled)
- // TextRegion::_setcursortype(1);
- // else
- // TextRegion::_setcursortype(0);
-}
-
-bool is_cursor_enabled()
-{
- // if (TextRegion::cursor_flag)
- // return (true);
-
- return (false);
-}
-
-bool is_smart_cursor_enabled()
-{
- return false;
-}
-
-void enable_smart_cursor(bool dummy)
-{
-}
-
-int wherex()
-{
- return tiles.wherex();
-}
-
-int wherey()
-{
- return tiles.wherey();
-}
-
-int get_number_of_lines()
-{
- return tiles.get_number_of_lines();
-}
-
-int get_number_of_cols()
-{
- return tiles.get_number_of_cols();
-}
-
-int getch_ck()
-{
- return (tiles.getch_ck());
-}
-
-int getchk()
-{
- return getch_ck();
-}
-
-void clrscr()
-{
- tiles.clrscr();
-}
-
-void cgotoxy(int x, int y, GotoRegion region)
-{
- tiles.cgotoxy(x, y, region);
-}
-
-coord_def cgetpos(GotoRegion region)
-{
- ASSERT(region == get_cursor_region());
- return (coord_def(wherex(), wherey()));
-}
-
-GotoRegion get_cursor_region()
-{
- return (tiles.get_cursor_region());
-}
-
-void delay(unsigned int ms)
-{
- tiles.redraw();
- fprintf(stdout, "delay(%d);\n", ms);
-
- if (ms)
- usleep(ms * 1000);
-}
-
-void update_screen()
-{
- tiles.set_need_redraw();
-}
-
-bool kbhit()
-{
- // Check stdin in a non-blocking way
- struct timeval tv;
- fd_set fds;
- tv.tv_sec = 0;
- tv.tv_usec = 0;
- FD_ZERO(&fds);
- FD_SET(STDIN_FILENO, &fds); //STDIN_FILENO is 0
- select(STDIN_FILENO+1, &fds, NULL, NULL, &tv);
- return FD_ISSET(STDIN_FILENO, &fds) != 0;
-}
-
-void console_startup()
-{
- tiles.resize();
-}
-
-void console_shutdown()
-{
- tiles.shutdown();
-}
-#endif // #ifdef USE_TILE_WEB
diff --git a/crawl-ref/source/main.cc b/crawl-ref/source/main.cc
index c1f0188ca8..2388575de9 100644
--- a/crawl-ref/source/main.cc
+++ b/crawl-ref/source/main.cc
@@ -312,10 +312,12 @@ static void _reset_game()
note_list.clear();
msg::deinitialise_mpr_streams();
-#ifdef USE_TILE
+#ifdef USE_TILE_LOCAL
// [ds] Don't show the title screen again, just go back to
// the menu.
crawl_state.title_screen = false;
+#endif
+#ifdef USE_TILE
tiles.clear_text_tags(TAG_NAMED_MONSTER);
#endif
}
@@ -1008,9 +1010,9 @@ bool apply_berserk_penalty = false;
static void _center_cursor()
{
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
const coord_def cwhere = crawl_view.grid2screen(you.pos());
- cgotoxy(cwhere.x, cwhere.y);
+ cgotoxy(cwhere.x, cwhere.y, GOTO_DNGN);
#endif
}
@@ -1227,7 +1229,7 @@ static void _input()
crawl_state.waiting_for_command = true;
c_input_reset(true);
-#ifdef USE_TILE
+#ifdef USE_TILE_LOCAL
cursor_control con(false);
#endif
const command_type cmd = _get_next_cmd();
@@ -1717,7 +1719,7 @@ static void _do_display_map()
if (Hints.hints_events[HINT_MAP_VIEW])
Hints.hints_events[HINT_MAP_VIEW] = false;
-#ifdef USE_TILE
+#ifdef USE_TILE_LOCAL
// Since there's no actual overview map, but the functionality
// exists, give a message to explain what's going on.
mpr("Move the cursor to view the level map, or type <w>?</w> for "
@@ -1728,7 +1730,7 @@ static void _do_display_map()
level_pos pos;
const bool travel = show_map(pos, true, true, true);
-#ifdef USE_TILE
+#ifdef USE_TILE_LOCAL
mpr("Returning to the game...");
#endif
if (travel)
@@ -2032,7 +2034,7 @@ void process_command(command_type cmd)
// because we want to have CTRL-Y available...
// and unfortunately they tend to be stuck together.
clrscr();
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
console_shutdown();
kill(0, SIGTSTP);
console_startup();
diff --git a/crawl-ref/source/output.cc b/crawl-ref/source/output.cc
index 14fcfe860d..f7da086626 100644
--- a/crawl-ref/source/output.cc
+++ b/crawl-ref/source/output.cc
@@ -107,7 +107,7 @@ class colour_bar
textcolor(BLACK);
for (int cx = 0; cx < width; cx++)
{
-#ifdef USE_TILE
+#ifdef USE_TILE_LOCAL
// Maybe this should use textbackground too?
textcolor(BLACK + m_empty * 16);
@@ -162,7 +162,7 @@ class colour_bar
colour_bar HP_Bar(LIGHTGREEN, GREEN, RED, DARKGREY);
-#ifdef USE_TILE
+#ifdef USE_TILE_LOCAL
colour_bar MP_Bar(BLUE, BLUE, LIGHTBLUE, DARKGREY);
#else
colour_bar MP_Bar(LIGHTBLUE, BLUE, MAGENTA, DARKGREY);
@@ -684,7 +684,7 @@ static void _print_status_lights(int y)
}
}
-#ifdef USE_TILE
+#ifdef USE_TILE_LOCAL
static bool _need_stats_printed()
{
return you.redraw_hit_points
@@ -714,7 +714,7 @@ void print_stats(void)
if (MP_Bar.wants_redraw())
you.redraw_magic_points = true;
-#ifdef USE_TILE
+#ifdef USE_TILE_LOCAL
bool has_changed = _need_stats_printed();
#endif
@@ -806,7 +806,7 @@ void print_stats(void)
}
textcolor(LIGHTGREY);
-#ifdef USE_TILE
+#ifdef USE_TILE_LOCAL
if (has_changed)
update_screen();
#else
@@ -1085,7 +1085,7 @@ std::string mpr_monster_list(bool past)
return (msg);
}
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
static void _print_next_monster_desc(const std::vector<monster_info>& mons,
int& start, bool zombified = false,
int idx = -1)
@@ -1190,7 +1190,7 @@ static void _print_next_monster_desc(const std::vector<monster_info>& mons,
}
#endif
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
// #define BOTTOM_JUSTIFY_MONSTER_LIST
// Returns -1 if the monster list is empty, 0 if there are so many monsters
// they have to be consolidated, and 1 otherwise.
@@ -1879,7 +1879,7 @@ static char _get_overview_screen_results()
formatted_scroller overview;
overview.set_flags(MF_SINGLESELECT | MF_ALWAYS_SHOW_MORE | MF_NOWRAP);
overview.set_more(formatted_string::parse_string(
-#ifdef USE_TILE
+#ifdef USE_TILE_LOCAL
"<cyan>[ +/L-click : Page down. - : Page up."
" Esc/R-click exits.]"));
#else
diff --git a/crawl-ref/source/state.cc b/crawl-ref/source/state.cc
index facc854f29..b2b482677b 100644
--- a/crawl-ref/source/state.cc
+++ b/crawl-ref/source/state.cc
@@ -38,7 +38,7 @@ game_state::game_state()
repeat_cmd(CMD_NO_CMD),cmd_repeat_started_unsafe(false),
lua_calls_no_turn(0), stat_gain_prompt(false),
level_annotation_shown(false),
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
mlist_targeting(false),
#else
title_screen(true),
diff --git a/crawl-ref/source/state.h b/crawl-ref/source/state.h
index d58bf068b7..5714ab1577 100644
--- a/crawl-ref/source/state.h
+++ b/crawl-ref/source/state.h
@@ -89,7 +89,7 @@ struct game_state
bool level_annotation_shown;
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
// Are we currently targeting using the mlist?
// This is global because the monster pane uses this when
// drawing.
diff --git a/crawl-ref/source/tileweb.cc b/crawl-ref/source/tileweb.cc
index 0d3cf87aed..58021f9706 100644
--- a/crawl-ref/source/tileweb.cc
+++ b/crawl-ref/source/tileweb.cc
@@ -33,7 +33,11 @@
#include "viewgeom.h"
#include <sys/time.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/un.h>
#include <stdarg.h>
+#include <errno.h>
@@ -49,7 +53,8 @@ static unsigned int get_milliseconds()
TilesFramework tiles;
TilesFramework::TilesFramework()
- : m_view_loaded(false),
+ : m_crt_enabled(true),
+ m_view_loaded(false),
m_next_view_tl(0, 0),
m_next_view_br(-1, -1),
m_current_flash_colour(BLACK),
@@ -72,6 +77,8 @@ TilesFramework::~TilesFramework()
void TilesFramework::shutdown()
{
+ close(m_sock);
+ remove(m_sock_name.c_str());
}
void TilesFramework::draw_doll_edit()
@@ -80,6 +87,30 @@ void TilesFramework::draw_doll_edit()
bool TilesFramework::initialise()
{
+ // Init socket
+ m_sock = socket(PF_UNIX, SOCK_DGRAM, 0);
+ if (m_sock < 0)
+ {
+ die("Can't open the webtiles socket!");
+ }
+ sockaddr_un addr;
+ addr.sun_family = AF_UNIX;
+ strcpy(addr.sun_path, m_sock_name.c_str());
+ if (bind(m_sock, (sockaddr*) &addr, sizeof (sockaddr_un)))
+ {
+ die("Can't bind the webtiles socket!");
+ }
+
+ int bufsize = 64 * 1024;
+ if (setsockopt(m_sock, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof (bufsize)))
+ {
+ die("Can't set buffer size!");
+ }
+ m_max_msg_size = bufsize;
+
+ if (m_await_connection)
+ _await_connection();
+
std::string title = CRAWL " " + Version::Long();
send_message("document.title = \"%s\";", title.c_str());
@@ -95,36 +126,196 @@ bool TilesFramework::initialise()
void TilesFramework::write_message(const char *format, ...)
{
for (unsigned int i = 0; i < m_prefixes.size(); ++i)
- fputs(m_prefixes[i].c_str(), stdout);
+ m_msg_buf.append(m_prefixes[i].data());
m_prefixes.clear();
+ char buf[2048];
+ int len;
+
va_list argp;
va_start(argp, format);
- vfprintf(stdout, format, argp);
+ if ((len = vsnprintf(buf, sizeof (buf), format, argp)) >= sizeof (buf))
+ die("Webtiles message too long! (%d)", len);
va_end(argp);
+
+ m_msg_buf.append(buf);
}
void TilesFramework::finish_message()
{
- fprintf(stdout, "\n");
- fflush(stdout);
+ m_msg_buf.append("\n");
+ const char* fragment_start = m_msg_buf.data();
+ const char* data_end = m_msg_buf.data() + m_msg_buf.size();
+ while (fragment_start < data_end)
+ {
+ int fragment_size = data_end - fragment_start;
+ if (fragment_size > m_max_msg_size)
+ fragment_size = m_max_msg_size;
+
+ for (unsigned int i = 0; i < m_dest_addrs.size(); ++i)
+ {
+ if (sendto(m_sock, fragment_start, fragment_size, 0,
+ (sockaddr*) &m_dest_addrs[i], sizeof (sockaddr_un)) == -1)
+ {
+ if (errno == ECONNREFUSED || errno == ENOENT)
+ {
+ // the other side is dead
+ m_dest_addrs.erase(m_dest_addrs.begin() + i);
+ i--;
+ }
+ else
+ die("Socket write error: %s", strerror(errno));
+ }
+ }
+
+ fragment_start += fragment_size;
+ }
m_prefixes.clear();
+ m_msg_buf.clear();
}
void TilesFramework::send_message(const char *format, ...)
{
for (unsigned int i = 0; i < m_prefixes.size(); ++i)
- fputs(m_prefixes[i].c_str(), stdout);
+ m_msg_buf.append(m_prefixes[i].data());
m_prefixes.clear();
+ char buf[2048];
+ int len;
+
va_list argp;
va_start(argp, format);
- vfprintf(stdout, format, argp);
+ if ((len = vsnprintf(buf, sizeof (buf), format, argp)) >= sizeof (buf))
+ die("Webtiles message too long! (%d)", len);
va_end(argp);
+ m_msg_buf.append(buf);
+
finish_message();
}
+void TilesFramework::_await_connection()
+{
+ while (m_dest_addrs.size() == 0)
+ {
+ _receive_control_message();
+ }
+}
+
+wint_t TilesFramework::_receive_control_message()
+{
+ char buf[4096]; // Should be enough for client->server messages
+ sockaddr_un srcaddr;
+ socklen_t srcaddr_len;
+
+ srcaddr_len = sizeof (srcaddr);
+
+ int len = recvfrom(m_sock, buf, sizeof (buf),
+ 0,
+ (sockaddr *) &srcaddr, &srcaddr_len);
+
+ if (len == -1)
+ {
+ die("Socket read error: %s", strerror(errno));
+ }
+
+ std::string data(buf, len);
+ return _handle_control_message(srcaddr, data);
+}
+
+wint_t TilesFramework::_handle_control_message(sockaddr_un addr, std::string data)
+{
+ // Hack - this needs a real JSON parser
+
+ static const std::string keymsgstart("{\"msg\":\"key\",\"keycode\":");
+
+ int c = 0;
+
+ if (data == "{\"msg\":\"attach\"}")
+ {
+ m_dest_addrs.push_back(addr);
+ }
+ else if (data.compare(0, keymsgstart.size(), keymsgstart) == 0)
+ {
+ std::stringstream ss(data);
+ ss.ignore(keymsgstart.size());
+
+ ss >> c;
+
+ if (ss.fail())
+ c = 0;
+ }
+ else if (data == "{\"msg\":\"spectator_joined\"}")
+ {
+ _send_everything();
+ }
+
+ return c;
+}
+
+bool TilesFramework::await_input(wint_t& c, bool block)
+{
+ int result;
+ fd_set fds;
+ int maxfd = m_sock;
+
+ while (true)
+ {
+ do
+ {
+ FD_ZERO(&fds);
+ FD_SET(STDIN_FILENO, &fds);
+ FD_SET(m_sock, &fds);
+
+ if (block)
+ {
+ result = select(maxfd + 1, &fds, NULL, NULL, NULL);
+ }
+ else
+ {
+ timeval timeout;
+ timeout.tv_sec = 0;
+ timeout.tv_usec = 0;
+
+ result = select(maxfd + 1, &fds, NULL, NULL, &timeout);
+ }
+ }
+ while (result == -1 && errno == EINTR);
+
+ if (result == 0)
+ {
+ return false;
+ }
+ else if (result > 0)
+ {
+ if (FD_ISSET(m_sock, &fds))
+ {
+ c = _receive_control_message();
+
+ if (c > 0)
+ return true;
+ }
+
+ if (FD_ISSET(STDIN_FILENO, &fds))
+ {
+ c = 0;
+ return true;
+ }
+ }
+ else if (errno == EBADF)
+ {
+ // This probably means that stdin got closed because of a
+ // SIGHUP. We'll just return.
+ c = 0;
+ return false;
+ }
+ else
+ {
+ die("select error: %s", strerror(errno));
+ }
+ }
+}
+
void TilesFramework::push_prefix(std::string prefix)
{
m_prefixes.push_back(prefix);
@@ -612,131 +803,65 @@ void TilesFramework::load_dungeon(const coord_def &cen)
unwind_var<coord_def> vlos1(crawl_view.vlos1);
unwind_var<coord_def> vlos2(crawl_view.vlos2);
+ m_next_gc = cen;
+
crawl_view.calc_vlos();
- viewwindow(false);
+ viewwindow(false, true);
place_cursor(CURSOR_MAP, cen);
}
static const int min_stat_height = 12;
static const int stat_width = 42;
-static void _send_layout_data(bool need_response)
+static void _send_layout_data()
{
- // need_response indicates if the client needs to set a layout
tiles.send_message("layout({view_max_width:%u,view_max_height:%u,\
force_overlay:%u,show_diameter:%u,msg_min_height:%u,stat_width:%u, \
-min_stat_height:%u,gxm:%u,gym:%u},%u);",
+min_stat_height:%u,gxm:%u,gym:%u});",
Options.view_max_width, Options.view_max_height,
Options.tile_force_overlay, ENV_SHOW_DIAMETER,
Options.msg_min_height, stat_width,
min_stat_height + (Options.show_gold_turns ? 1 : 0),
- GXM, GYM,
- need_response);
+ GXM, GYM);
}
void TilesFramework::resize()
{
- // Width of status area in characters.
- crawl_view.hudsz.x = stat_width;
- crawl_view.msgsz.y = Options.msg_min_height;
- m_text_message.resize(crawl_view.msgsz.x, crawl_view.msgsz.y);
-
- crawl_view.viewsz = coord_def(ENV_SHOW_DIAMETER, ENV_SHOW_DIAMETER);
- crawl_view.init_view();
-
// Send the client the necessary data to do the layout
- _send_layout_data(true);
+ _send_layout_data();
- // Now wait for the response
- getch_ck();
+ m_text_message.resize(crawl_view.msgsz.x, crawl_view.msgsz.y);
+ m_text_stat.resize(crawl_view.hudsz.x, crawl_view.hudsz.y);
+ m_text_crt.resize(crawl_view.termsz.x, crawl_view.termsz.y);
}
-int TilesFramework::getch_ck()
+/*
+ Send everything a newly joined spectator needs
+ */
+void TilesFramework::_send_everything()
{
- m_text_crt.send();
- m_text_stat.send();
- m_text_message.send();
-
- if (need_redraw())
- redraw();
-
- int key = getchar();
- if (key == '\\')
- {
- // Char encoded as a number
- char data[10];
- fgets(data, 10, stdin);
- return atoi(data);
- }
- else if (key == '^')
+ std::string title = CRAWL " " + Version::Long();
+ send_message("document.title = \"%s\";", title.c_str());
+ m_text_crt.send(true);
+ m_text_stat.send(true);
+ m_text_message.send(true);
+ _send_layout_data();
+ send_message("vgrdc(%d,%d);",
+ m_current_gc.x - m_origin.x, m_current_gc.y - m_origin.y);
+ send_message("set_flash(%d);", m_current_flash_colour);
+ _send_map(true);
+ switch (m_active_layer)
{
- // Control messages
- // TODO: This would be much nicer if we just sent messages in JSON
- int msg = getchar();
- int num = 0;
- if (msg == 'w' || msg == 'h' || msg == 's'
- || msg == 'W' || msg == 'H' || msg == 'm')
- {
- // Read the number
- char data[10];
- fgets(data, 10, stdin);
- num = atoi(data);
- }
- switch (msg)
- {
- case 's': // Set height of the stats area
- if (num <= 0) num = 1;
- if (num > 400) num = 400;
- crawl_view.hudsz.y = num;
- m_text_stat.resize(crawl_view.hudsz.x, crawl_view.hudsz.y);
- break;
- case 'W': // Set width of CRT
- if (num <= 0) num = 1;
- if (num > 400) num = 400;
- crawl_view.termsz.x = num;
- m_text_crt.resize(crawl_view.termsz.x, crawl_view.termsz.y);
- break;
- case 'H': // Set height of CRT
- if (num <= 0) num = 1;
- if (num > 400) num = 400;
- crawl_view.termsz.y = num;
- m_text_crt.resize(crawl_view.termsz.x, crawl_view.termsz.y);
- break;
- case 'm': // Set width of the message view
- if (num <= 0) num = 1;
- if (num > 400) num = 400;
- crawl_view.msgsz.x = num;
- m_text_message.resize(crawl_view.msgsz.x, crawl_view.msgsz.y);
- break;
- case 'r': // A spectator joined, resend the necessary data
- std::string title = CRAWL " " + Version::Long();
- send_message("document.title = \"%s\";", title.c_str());
- m_text_crt.send(true);
- m_text_stat.send(true);
- m_text_message.send(true);
- _send_layout_data(false);
- send_message("vgrdc(%d,%d);",
- m_current_gc.x - m_origin.x, m_current_gc.y - m_origin.y);
- send_message("set_flash(%d);", m_current_flash_colour);
- _send_map(true);
- switch (m_active_layer)
- {
- case LAYER_CRT:
- send_message("set_layer('crt');");
- break;
- case LAYER_NORMAL:
- send_message("set_layer('normal');");
- break;
- default:
- // Cannot happen
- break;
- }
- break;
- }
-
- return getch_ck();
+ case LAYER_CRT:
+ send_message("set_layer('crt');");
+ break;
+ case LAYER_NORMAL:
+ send_message("set_layer('normal');");
+ break;
+ default:
+ // Cannot happen
+ break;
}
- return key;
}
void TilesFramework::clrscr()
@@ -744,30 +869,20 @@ void TilesFramework::clrscr()
// TODO: Clear cursor
m_text_crt.clear();
- m_text_message.clear();
- m_text_stat.clear();
- cgotoxy(1, 1);
+ this->cgotoxy(1, 1);
set_need_redraw();
}
-int TilesFramework::get_number_of_lines()
+void TilesFramework::set_crt_enabled(bool value)
{
- return m_text_crt.my;
+ m_crt_enabled = value;
}
-int TilesFramework::get_number_of_cols()
+bool TilesFramework::is_crt_enabled()
{
- switch (m_active_layer)
- {
- default:
- return 0;
- case LAYER_NORMAL:
- return m_text_message.mx;
- case LAYER_CRT:
- return m_text_crt.mx;
- }
+ return m_crt_enabled;
}
void TilesFramework::cgotoxy(int x, int y, GotoRegion region)
@@ -777,10 +892,17 @@ void TilesFramework::cgotoxy(int x, int y, GotoRegion region)
switch (region)
{
case GOTO_CRT:
- if (m_active_layer != LAYER_CRT)
- send_message("set_layer(\"crt\");");
- m_active_layer = LAYER_CRT;
- m_print_area = &m_text_crt;
+ if (m_crt_enabled > 0)
+ {
+ if (m_active_layer != LAYER_CRT)
+ send_message("set_layer(\"crt\");");
+ m_active_layer = LAYER_CRT;
+ m_print_area = &m_text_crt;
+ }
+ else
+ {
+ m_print_area = NULL;
+ }
break;
case GOTO_MSG:
if (m_active_layer != LAYER_NORMAL)
@@ -795,42 +917,40 @@ void TilesFramework::cgotoxy(int x, int y, GotoRegion region)
m_print_area = &m_text_stat;
break;
default:
- die("invalid cgotoxy region in webtiles: %d", region);
+ m_print_area = NULL;
break;
}
m_cursor_region = region;
}
-GotoRegion TilesFramework::get_cursor_region() const
-{
- return m_cursor_region;
-}
-
void TilesFramework::redraw()
{
m_text_crt.send();
m_text_stat.send();
m_text_message.send();
- if (m_current_gc != m_next_gc)
+ if (m_need_redraw)
{
- if (m_origin.equals(-1, -1))
- m_origin = m_next_gc;
- write_message("vgrdc(%d,%d);",
- m_next_gc.x - m_origin.x,
- m_next_gc.y - m_origin.y);
- m_current_gc = m_next_gc;
- }
+ if (m_current_gc != m_next_gc)
+ {
+ if (m_origin.equals(-1, -1))
+ m_origin = m_next_gc;
+ write_message("vgrdc(%d,%d);",
+ m_next_gc.x - m_origin.x,
+ m_next_gc.y - m_origin.y);
+ m_current_gc = m_next_gc;
+ }
- if (m_current_flash_colour != m_next_flash_colour)
- {
- write_message("set_flash(%d);",
- m_next_flash_colour);
- m_current_flash_colour = m_next_flash_colour;
- }
+ if (m_current_flash_colour != m_next_flash_colour)
+ {
+ write_message("set_flash(%d);",
+ m_next_flash_colour);
+ m_current_flash_colour = m_next_flash_colour;
+ }
- if (m_view_loaded)
- _send_map(false);
+ if (m_view_loaded)
+ _send_map(false);
+ }
m_need_redraw = false;
m_last_tick_redraw = get_milliseconds();
@@ -1013,6 +1133,9 @@ void TilesFramework::put_string(char *buffer)
void TilesFramework::put_ucs_string(ucs_t *str)
{
+ if (m_print_area == NULL)
+ return;
+
while (*str)
{
if (*str == '\r')
@@ -1048,6 +1171,9 @@ void TilesFramework::put_ucs_string(ucs_t *str)
void TilesFramework::clear_to_end_of_line()
{
+ if (m_print_area == NULL)
+ return;
+
for (int x = m_print_x; x < m_print_area->mx; ++x)
m_print_area->put_character(' ', m_print_fg, m_print_bg, x, m_print_y);
}
diff --git a/crawl-ref/source/tileweb.h b/crawl-ref/source/tileweb.h
index fd34a1e691..9c08072928 100644
--- a/crawl-ref/source/tileweb.h
+++ b/crawl-ref/source/tileweb.h
@@ -12,6 +12,7 @@
#include "viewgeom.h"
#include "map_knowledge.h"
#include <map>
+#include <sys/un.h>
class TilesFramework
{
@@ -28,9 +29,6 @@ public:
void clrscr();
void cgotoxy(int x, int y, GotoRegion region = GOTO_CRT);
- GotoRegion get_cursor_region() const;
- int get_number_of_lines();
- int get_number_of_cols();
void update_minimap(const coord_def &gc);
void clear_minimap();
@@ -60,13 +58,30 @@ public:
void put_string(char *str);
void put_ucs_string(ucs_t *str);
void clear_to_end_of_line();
- int wherex() { return m_print_x + 1; }
- int wherey() { return m_print_y + 1; }
void write_message(const char *format, ...);
void finish_message();
void send_message(const char *format, ...);
+ /* Webtiles can receive input both via stdin, and on the
+ socket. Also, while waiting for input, it should be
+ able to handle other control messages (for example,
+ requests to re-send data when a new spectator joins).
+
+ This function waits until input is available either via
+ stdin or from a control message. If the input came from
+ a control message, it will be written into c; otherwise,
+ it still has to be read from stdin.
+
+ If block is false, await_input will immediately return,
+ even if no input is available. The return value indicates
+ whether input can be read from stdin; c will be non-zero
+ if input came via a control message.
+ */
+ bool await_input(wint_t& c, bool block);
+
+ void check_for_control_messages();
+
/* Adds a prefix that will be written before any other
data that is sent after this call, unless no other
data is sent until pop_prefix is called. The suffix
@@ -75,7 +90,23 @@ public:
void push_prefix(std::string prefix);
void pop_prefix(std::string suffix);
bool prefix_popped();
+
+ std::string m_sock_name;
+ bool m_await_connection;
+
+ void set_crt_enabled(bool value);
+ bool is_crt_enabled();
+
protected:
+ int m_sock;
+ int m_max_msg_size;
+ std::string m_msg_buf;
+ std::vector<sockaddr_un> m_dest_addrs;
+
+ void _await_connection();
+ wint_t _handle_control_message(sockaddr_un addr, std::string data);
+ wint_t _receive_control_message();
+
std::vector<std::string> m_prefixes;
enum LayerID
@@ -86,6 +117,7 @@ protected:
LAYER_MAX,
};
LayerID m_active_layer;
+ bool m_crt_enabled;
unsigned int m_last_tick_redraw;
bool m_need_redraw;
@@ -126,6 +158,8 @@ protected:
dolls_data last_player_doll;
+ void _send_everything();
+
void _send_map(bool force_full = false);
void _send_cell(const coord_def &gc,
const screen_cell_t &current_sc, const screen_cell_t &next_sc,
@@ -140,4 +174,22 @@ protected:
// Main interface for tiles functions
extern TilesFramework tiles;
+class tiles_crt_control
+{
+public:
+ tiles_crt_control(bool crt_enabled)
+ : m_was_enabled(tiles.is_crt_enabled())
+ {
+ tiles.set_crt_enabled(crt_enabled);
+ }
+
+ ~tiles_crt_control()
+ {
+ tiles.set_crt_enabled(m_was_enabled);
+ }
+
+private:
+ bool m_was_enabled;
+};
+
#endif
diff --git a/crawl-ref/source/view.cc b/crawl-ref/source/view.cc
index 7979a98060..dfd02e3f5a 100644
--- a/crawl-ref/source/view.cc
+++ b/crawl-ref/source/view.cc
@@ -724,7 +724,7 @@ void view_update_at(const coord_def &pos)
show_update_at(pos);
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
if (!env.map_knowledge(pos).visible())
return;
glyph g = get_cell_glyph(pos);
@@ -750,7 +750,7 @@ void view_update_at(const coord_def &pos)
#endif
}
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
void flash_monster_colour(const monster* mon, uint8_t fmc_colour,
int fmc_delay)
{
@@ -1000,8 +1000,11 @@ static bool _show_terrain = false;
//
// If show_updates is set, env.show and dependent structures
// are updated. Should be set if anything in view has changed.
+//
+// If tiles_only is set, only the tile view will be updated. This
+// is only relevant for Webtiles.
//---------------------------------------------------------------
-void viewwindow(bool show_updates)
+void viewwindow(bool show_updates, bool tiles_only)
{
if (you.duration[DUR_TIME_STEP])
return;
@@ -1070,10 +1073,18 @@ void viewwindow(bool show_updates)
// and this simply works without requiring a stack.
you.flash_colour = BLACK;
you.last_view_update = you.num_turns;
-#ifndef USE_TILE
- puttext(crawl_view.viewp.x, crawl_view.viewp.y, crawl_view.vbuf);
- update_monster_pane();
-#else
+#ifndef USE_TILE_LOCAL
+#ifdef USE_TILE_WEB
+ tiles_crt_control crt(false);
+#endif
+
+ if (!tiles_only)
+ {
+ puttext(crawl_view.viewp.x, crawl_view.viewp.y, crawl_view.vbuf);
+ update_monster_pane();
+ }
+#endif
+#ifdef USE_TILE
tiles.set_need_redraw(you.running ? Options.tile_runrest_rate : 0);
tiles.load_dungeon(crawl_view.vbuf, crawl_view.vgrdc);
tiles.update_tabs();
@@ -1116,7 +1127,7 @@ void draw_cell(screen_cell_t *cell, const coord_def &gc,
{
if (you.see_cell(gc))
{
-#ifdef USE_TILE
+#ifdef USE_TILE_LOCAL
cell->colour = real_colour(flash_colour);
#else
monster_type mons = env.map_knowledge(gc).monster();
diff --git a/crawl-ref/source/view.h b/crawl-ref/source/view.h
index b6713f7eac..35751f2f94 100644
--- a/crawl-ref/source/view.h
+++ b/crawl-ref/source/view.h
@@ -35,12 +35,12 @@ bool view_update();
void view_update_at(const coord_def &pos);
void flash_view(uint8_t colour = BLACK); // inside #ifndef USE_TILE_LOCAL?
void flash_view_delay(uint8_t colour = BLACK, int delay = 150);
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
void flash_monster_colour(const monster* mon, uint8_t fmc_colour,
int fmc_delay);
#endif
-void viewwindow(bool show_updates = true);
+void viewwindow(bool show_updates = true, bool tiles_only = false);
void draw_cell(screen_cell_t *cell, const coord_def &gc,
bool anim_updates, int flash_colour);
diff --git a/crawl-ref/source/viewgeom.cc b/crawl-ref/source/viewgeom.cc
index d816dbbf3b..759578c023 100644
--- a/crawl-ref/source/viewgeom.cc
+++ b/crawl-ref/source/viewgeom.cc
@@ -51,7 +51,7 @@ class _layout
#pragma GCC diagnostic ignored "-Wstrict-overflow"
void _assert_validity() const
{
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
// Check that all the panes fit in the view.
ASSERT((viewp+viewsz - termp).x <= termsz.x);
ASSERT((viewp+viewsz - termp).y <= termsz.y);
@@ -378,7 +378,7 @@ void crawl_view_geometry::init_geometry()
const _inline_layout lay_inline(termsz, hudsz);
const _mlist_col_layout lay_mlist(termsz, hudsz);
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
if ((termsz.x < MIN_COLS || termsz.y < MIN_LINES || !lay_inline.valid)
&& !crawl_state.need_save)
{
@@ -405,7 +405,7 @@ void crawl_view_geometry::init_geometry()
mlistp = winner->mlistp;
mlistsz = winner->mlistsz;
-#ifdef USE_TILE
+#ifdef USE_TILE_LOCAL
// libgui may redefine these based on its own settings.
gui_init_view_params(*this);
#endif
diff --git a/crawl-ref/source/viewmap.cc b/crawl-ref/source/viewmap.cc
index dd600ce8a0..d818673abe 100644
--- a/crawl-ref/source/viewmap.cc
+++ b/crawl-ref/source/viewmap.cc
@@ -41,7 +41,7 @@
#include "tileview.h"
#endif
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
static unsigned _get_travel_colour(const coord_def& p)
{
#ifdef WIZARD
@@ -62,7 +62,7 @@ static unsigned _get_travel_colour(const coord_def& p)
}
#endif
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
static bool _travel_colour_override(const coord_def& p)
{
if (is_waypoint(p) || is_stair_exclusion(p)
@@ -431,7 +431,7 @@ static int _get_number_of_lines_levelmap()
return get_number_of_lines() - (Options.level_map_title ? 1 : 0);
}
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
static void _draw_level_map(int start_x, int start_y, bool travel_mode,
bool on_level)
{
@@ -503,7 +503,7 @@ static void _draw_level_map(int start_x, int start_y, bool travel_mode,
puttext(1, top, vbuf);
}
-#endif // USE_TILE
+#endif // USE_TILE_LOCAL
static void _reset_travel_colours(std::vector<coord_def> &features,
bool on_level)
@@ -530,7 +530,7 @@ static bool _comp_glyphs(const glyph& g1, const glyph& g2)
return (g1.ch < g2.ch || g1.ch == g2.ch && g1.col < g2.col);
}
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
static glyph _get_feat_glyph(const coord_def& gc);
#endif
@@ -573,7 +573,7 @@ class feature_list
void maybe_add(const coord_def& gc)
{
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
if (!env.map_knowledge(gc).known())
return;
@@ -604,7 +604,7 @@ public:
}
};
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
static void _draw_title(const coord_def& cpos, const feature_list& feats)
{
if (!Options.level_map_title)
@@ -718,6 +718,10 @@ bool show_map(level_pos &lpos,
cursor_control ccon(!Options.use_fake_cursor);
int i, j;
+#ifdef USE_TILE_WEB
+ tiles_crt_control crt(false);
+#endif
+
int move_x = 0, move_y = 0, scroll_y = 0;
bool new_level = true;
@@ -750,7 +754,7 @@ bool show_map(level_pos &lpos,
bool map_alive = true;
bool redraw_map = true;
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
clrscr();
#endif
textcolor(DARKGREY);
@@ -871,12 +875,13 @@ bool show_map(level_pos &lpos,
// location. It silently ignores everything else going
// on in this function. --Enne
tiles.load_dungeon(lpos.pos);
-#else
+#endif
+#ifndef USE_TILE_LOCAL
_draw_title(lpos.pos, feats);
_draw_level_map(start_x, start_y, travel_mode, on_level);
-#endif // USE_TILE
+#endif
}
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
cursorxy(curs_x, curs_y + top - 1);
#endif
redraw_map = true;
@@ -1248,7 +1253,7 @@ bool show_map(level_pos &lpos,
lpos.pos.y = std::min(std::max(lpos.pos.y, min_y), max_y);
move_x = lpos.pos.x - oldp.x;
move_y = lpos.pos.y - oldp.y;
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
if (num_lines < map_lines)
{
// Scrolling only happens when we don't have a large enough
@@ -1293,7 +1298,7 @@ bool emphasise(const coord_def& where)
&& you.where_are_you != BRANCH_VESTIBULE_OF_HELL);
}
-#ifndef USE_TILE
+#ifndef USE_TILE_LOCAL
// Get glyph for feature list; here because it's so similar
// to get_map_col.
static glyph _get_feat_glyph(const coord_def& gc)
diff --git a/crawl-ref/source/webserver/config.py b/crawl-ref/source/webserver/config.py
index 62af85001a..23ad8cfd18 100644
--- a/crawl-ref/source/webserver/config.py
+++ b/crawl-ref/source/webserver/config.py
@@ -18,6 +18,8 @@ password_db = "./webserver/passwd.db3"
static_path = "./webserver/static"
template_path = "./webserver/templates/"
+server_socket_path = None # Uses global temp dir
+
games = OrderedDict([
("dcss-web-trunk", dict(
name = "DCSS trunk",
@@ -26,6 +28,7 @@ games = OrderedDict([
macro_path = "./rcs/",
morgue_path = "./rcs",
running_game_path = "./rcs/running",
+ socket_path = "./rcs",
client_prefix = "game")),
("sprint-web-trunk", dict(
name = "Sprint trunk",
@@ -34,6 +37,7 @@ games = OrderedDict([
macro_path = "./rcs/",
morgue_path = "./rcs",
running_game_path = "./rcs/running",
+ socket_path = "./rcs",
options = ["-sprint"],
client_prefix = "game")),
("zd-web-trunk", dict(
@@ -43,6 +47,7 @@ games = OrderedDict([
macro_path = "./rcs/",
morgue_path = "./rcs",
running_game_path = "./rcs/running",
+ socket_path = "./rcs",
options = ["-zotdef"],
client_prefix = "game")),
("tut-web-trunk", dict(
@@ -52,6 +57,7 @@ games = OrderedDict([
macro_path = "./rcs/",
morgue_path = "./rcs",
running_game_path = "./rcs/running",
+ socket_path = "./rcs",
options = ["-tutorial"],
client_prefix = "game")),
])
diff --git a/crawl-ref/source/webserver/connection.py b/crawl-ref/source/webserver/connection.py
new file mode 100644
index 0000000000..e5675aa561
--- /dev/null
+++ b/crawl-ref/source/webserver/connection.py
@@ -0,0 +1,81 @@
+import socket
+import fcntl
+import os, os.path
+import time
+import warnings
+
+from config import server_socket_path
+
+class WebtilesSocketConnection(object):
+ def __init__(self, io_loop, socketpath):
+ self.io_loop = io_loop
+ self.crawl_socketpath = socketpath
+ self.message_callback = None
+ self.socket = None
+ self.socketpath = None
+
+ self.msg_buffer = None
+
+ def connect(self):
+ if not os.path.exists(self.crawl_socketpath):
+ # Wait until the socket exists
+ self.io_loop.add_timeout(time.time() + 1, self.connect)
+ return
+
+ self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
+
+ # Set close-on-exec
+ flags = fcntl.fcntl(self.socket.fileno(), fcntl.F_GETFD)
+ fcntl.fcntl(self.socket.fileno(), flags | fcntl.FD_CLOEXEC)
+
+ self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+
+ # Bind to a temp path
+ # Ignore the security warning about tempnam; in this case,
+ # there is no security risk (the most that can happen is that
+ # the bind call fails)
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ self.socketpath = os.tempnam(server_socket_path, "crawl")
+ self.socket.bind(self.socketpath)
+
+ # Install handler
+ self.io_loop.add_handler(self.socket.fileno(),
+ self._handle_read,
+ self.io_loop.ERROR | self.io_loop.READ)
+
+ self.socket.sendto('{"msg":"attach"}', self.crawl_socketpath)
+
+ def _handle_read(self, fd, events):
+ if events & self.io_loop.READ:
+ data = self.socket.recv(128 * 1024, socket.MSG_DONTWAIT)
+
+ self._handle_data(data)
+
+ if events & self.io_loop.ERROR:
+ pass
+
+ def _handle_data(self, data):
+ if self.msg_buffer is not None:
+ data = self.msg_buffer + data
+
+ if data[-1] != "\n":
+ # All messages from crawl end with \n.
+ # If this one doesn't, it's fragmented.
+ self.msg_buffer = data
+
+ else:
+ self.msg_buffer = None
+
+ if self.message_callback:
+ self.message_callback(data)
+
+ def send_message(self, data):
+ self.socket.sendto(data, self.crawl_socketpath)
+
+ def close(self):
+ if self.socket:
+ self.io_loop.remove_handler(self.socket.fileno())
+ self.socket.close()
+ os.remove(self.socketpath)
+ self.socket = None
diff --git a/crawl-ref/source/webserver/process_handler.py b/crawl-ref/source/webserver/process_handler.py
new file mode 100644
index 0000000000..08c2521e2b
--- /dev/null
+++ b/crawl-ref/source/webserver/process_handler.py
@@ -0,0 +1,219 @@
+import os, os.path, errno
+import subprocess
+import datetime, time
+
+import config
+
+from tornado.escape import json_decode, json_encode
+
+from terminal import TerminalRecorder
+from connection import WebtilesSocketConnection
+
+class CrawlProcessHandlerBase(object):
+ def __init__(self, game_params, username, logger, io_loop):
+ self.game_params = game_params
+ self.username = username
+ self.logger = logger
+ self.io_loop = io_loop
+
+ self.process = None
+ self.client_version = None
+ self.where = None
+ self.wheretime = time.time()
+ self.kill_timeout = None
+
+ self.end_callback = None
+ self._watchers = set()
+ self.last_activity_time = time.time()
+
+ def idle_time(self):
+ return time.time() - self.last_activity_time
+
+ def send_to_all(self, msg):
+ for watcher in self._watchers:
+ watcher.handle_message(msg)
+
+ def update_watcher_description(self):
+ watcher_names = [watcher.username for watcher in self._watchers
+ if watcher.username]
+ anon_count = len(self._watchers) - len(watcher_names)
+ s = ", ".join(watcher_names)
+ if len(watcher_names) > 0 and anon_count > 0:
+ s = s + ", and %i Anon" % anon_count
+ elif anon_count > 0:
+ s = "%i Anon" % anon_count
+ self.send_to_all("watchers(%i,'%s')" %
+ (len(self._watchers), s))
+
+ def add_watcher(self, watcher):
+ self._watchers.add(watcher)
+ if self.client_version:
+ watcher.set_client(self.game_params, self.client_version)
+
+ def remove_watcher(self, watcher):
+ self._watchers.remove(watcher)
+ self.update_watcher_description()
+
+ def watcher_count(self):
+ return len(self._watchers)
+
+ def stop(self):
+ self.process.send_signal(subprocess.signal.SIGHUP)
+ t = time.time() + config.kill_timeout
+ self.kill_timeout = self.io_loop.add_timeout(t, self.kill)
+
+ def kill(self):
+ if self.process:
+ self.logger.info("Killing crawl process after SIGHUP did nothing.")
+ self.process.send_signal(subprocess.signal.SIGTERM)
+ self.kill_timeout = None
+
+ def check_where(self):
+ morgue_path = self.game_params["morgue_path"]
+ wherefile = os.path.join(morgue_path, self.username,
+ self.username + ".dglwhere")
+ try:
+ if os.path.getmtime(wherefile) > self.wheretime:
+ self.wheretime = time.time()
+ f = open(wherefile, "r")
+ _, _, newwhere = f.readline().partition("|")
+ f.close()
+
+ newwhere = newwhere.strip()
+
+ if self.where != newwhere:
+ self.where = newwhere
+ except (OSError, IOError):
+ pass
+
+ def _base_call(self):
+ game = self.game_params
+
+ call = [game["crawl_binary"],
+ "-name", self.username,
+ "-rc", os.path.join(game["rcfile_path"],
+ self.username + ".rc"),
+ "-macro", os.path.join(game["macro_path"],
+ self.username + ".macro"),
+ "-morgue", os.path.join(game["morgue_path"],
+ self.username)]
+
+ if "options" in game:
+ call += game["options"]
+
+ return call
+
+ def handle_input(self, msg):
+ raise NotImplementedError()
+
+class CrawlProcessHandler(CrawlProcessHandlerBase):
+ def __init__(self, game_params, username, logger, io_loop):
+ super(CrawlProcessHandler, self).__init__(game_params, username,
+ logger, io_loop)
+ self.socketpath = None
+ self.conn = None
+ self.ttyrec_filename = None
+
+ if "client_prefix" in game_params:
+ self.client_version = game_params["client_prefix"]
+
+
+ def start(self):
+ self.socketpath = os.path.join(self.game_params["socket_path"],
+ self.username + ".sock")
+
+ try: # Unlink if necessary
+ os.unlink(self.socketpath)
+ except OSError, e:
+ if e.errno != errno.ENOENT:
+ raise
+
+ game = self.game_params
+
+ call = self._base_call() + ["-webtiles-socket", self.socketpath,
+ "-await-connection"]
+
+ now = datetime.datetime.utcnow()
+ running_game_path = game["running_game_path"]
+ self.ttyrec_filename = os.path.join(running_game_path,
+ self.username + ":" +
+ now.strftime("%Y-%m-%d.%H:%M:%S")
+ + ".ttyrec")
+
+ self.logger.info("Starting crawl.")
+
+ self.process = TerminalRecorder(call, self.ttyrec_filename,
+ self.io_loop)
+ self.process.end_callback = self._on_process_end
+ self.process.output_callback = self._on_process_output
+
+ self.conn = WebtilesSocketConnection(self.io_loop, self.socketpath)
+ self.conn.message_callback = self._on_socket_message
+ self.conn.connect()
+
+ self.last_activity_time = time.time()
+
+ def _on_process_end(self):
+ self.logger.info("Crawl terminated.")
+
+ self.process = None
+
+ if self.conn:
+ self.conn.close()
+ self.conn = None
+
+ if self.kill_timeout:
+ self.io_loop.remove_timeout(self.kill_timeout)
+ self.kill_timeout = None
+
+ for watcher in list(self._watchers):
+ watcher.stop_watching()
+
+ if self.end_callback:
+ self.end_callback()
+
+ def add_watcher(self, watcher):
+ super(CrawlProcessHandler, self).add_watcher(watcher)
+
+ if self.conn:
+ self.conn.send_message('{"msg":"spectator_joined"}')
+
+ def handle_input(self, msg):
+ if msg.startswith("{"):
+ obj = json_decode(msg)
+
+ if obj["msg"] == "input" and self.process:
+ self.last_action_time = time.time()
+
+ data = ""
+ for x in obj["data"]:
+ data += chr(x)
+
+ self.process.poll()
+ if self.process:
+ self.process.write_input(data)
+
+ elif self.conn:
+ self.conn.send_message(msg.encode("utf8"))
+
+ else:
+ self.process.poll()
+ if self.process:
+ self.process.write_input(msg.encode("utf8"))
+
+ def _on_process_output(self, line):
+ self.check_where()
+ self.last_activity_time = time.time()
+
+ self.send_to_all(line)
+
+ def _on_socket_message(self, msg):
+ # stdout data is only used for compatibility to wrapper
+ # scripts -- so as soon as we receive something on the socket,
+ # we stop using stdout
+ self.process.output_callback = None
+
+ self.check_where()
+ self.last_activity_time = time.time()
+
+ self.send_to_all(msg)
diff --git a/crawl-ref/source/webserver/server.py b/crawl-ref/source/webserver/server.py
index 5737b70083..51233a7bf5 100755
--- a/crawl-ref/source/webserver/server.py
+++ b/crawl-ref/source/webserver/server.py
@@ -13,21 +13,22 @@ import crypt
import sqlite3
import logging
-import sys
import signal
import time, datetime
-import collections
import re
import random
import codecs
from config import *
+from process_handler import CrawlProcessHandler
+
logging.basicConfig(**logging_config)
class TornadoFilter(logging.Filter):
def filter(self, record):
- if record.module == "web" and record.levelno <= logging.INFO: return False
+ if record.module == "web" and record.levelno <= logging.INFO:
+ return False
return True
logging.getLogger().addFilter(TornadoFilter())
logging.addLevelName(logging.DEBUG, "DEBG")
@@ -121,15 +122,10 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler):
def __init__(self, app, req, **kwargs):
tornado.websocket.WebSocketHandler.__init__(self, app, req, **kwargs)
self.username = None
- self.p = None
self.timeout = None
- self.kill_timeout = None
- self.last_action_time = time.time()
self.watched_game = None
- self.watchers = set()
- self.ttyrec_filename = None
- self.where = None
- self.wheretime = time.time()
+ self.process = None
+ self.game_id = None
self.ioloop = tornado.ioloop.IOLoop.instance()
@@ -137,6 +133,12 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler):
self.id = current_id
current_id += 1
+ self.logger = logging.LoggerAdapter(logging.getLogger(), {})
+ self.logger.process = self._process_log_msg
+
+ def _process_log_msg(self, msg, kwargs):
+ return "#%-5s %s" % (self.id, msg), kwargs
+
def __hash__(self):
return self.id
@@ -144,16 +146,16 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler):
return self.id == other.id
def open(self):
- logging.info("#%s: Socket opened from ip %s (fd%s).", self.id,
- self.request.remote_ip, self.request.connection.stream.socket.fileno())
- global sockets
+ self.logger.info("Socket opened from ip %s (fd%s).",
+ self.request.remote_ip,
+ self.request.connection.stream.socket.fileno())
sockets.add(self)
self.reset_timeout()
if max_connections < len(sockets):
self.write_message("connection_closed('The maximum number of connections "
- + "has been reached, sorry :(');");
+ + "has been reached, sorry :(');")
self.close()
elif shutting_down:
self.close()
@@ -164,22 +166,20 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler):
self.start_crawl(None)
def idle_time(self):
- return time.time() - self.last_action_time
+ return self.process.idle_time()
def is_running(self):
- return self.p is not None
+ return self.process is not None
def is_in_lobby(self):
return not self.is_running() and self.watched_game is None
def update_lobby(self):
if self.client_terminated:
- logging.warn("#%s: update_lobby called for closed " +
- "socket! (Crawl is %srunning)" %
- (self.id,
- ("not " if self.is_running() else "")))
+ self.logger.warning("update_lobby called for closed " +
+ "socket! (Crawl is %srunning)",
+ "not " if self.is_running() else "")
if not self.is_running():
- global sockets
sockets.remove(self)
return
@@ -192,7 +192,7 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler):
# Rerender Banner
banner_html = self.render_string("banner.html", username = self.username)
self.write_message("$('#banner').html(" +
- tornado.escape.json_encode(banner_html) + ");");
+ tornado.escape.json_encode(banner_html) + ");")
play_html = self.render_string("game_links.html", games = games)
self.write_message("$('#play_now').html(" +
tornado.escape.json_encode(play_html) + ");")
@@ -210,14 +210,12 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler):
self.timeout = None
if not self.received_pong:
- logging.info("#%s: Connection to remote ip %s timed out.",
- self.id, self.request.remote_ip)
+ self.logger.info("Connection timed out.")
self.close()
else:
- if self.is_running() and self.idle_time() > max_idle_time:
- logging.info("#%s: Stopping crawl after idle time limit for %s.",
- self.id, self.username)
- self.stop_crawl()
+ if self.is_running() and self.process.idle_time() > max_idle_time:
+ self.logger.info("Stopping crawl after idle time limit.")
+ self.process.stop()
if not self.client_terminated:
self.reset_timeout()
@@ -228,63 +226,42 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler):
self.write_message("go_lobby();")
return
- if game_id not in games: return
-
- self.last_action_time = time.time()
+ if game_id not in games: return
self.game_id = game_id
- if dgl_mode:
- game = games[game_id]
-
- call = [game["crawl_binary"],
- "-name", self.username,
- "-rc", os.path.join(game["rcfile_path"], self.username + ".rc"),
- "-macro", os.path.join(game["macro_path"], self.username + ".macro"),
- "-morgue", os.path.join(game["morgue_path"], self.username)]
-
- if "options" in game:
- call += game["options"]
-
- if "client_prefix" in game:
- self.client_prefix = game["client_prefix"]
- self.send_client(self.client_prefix)
- else:
- call = ["./crawl"]
- self.send_client("game")
-
- self.p = subprocess.Popen(call,
- stdin = subprocess.PIPE,
- stdout = subprocess.PIPE,
- stderr = subprocess.PIPE)
-
- self.ioloop.add_handler(self.p.stdout.fileno(), self.on_stdout,
- self.ioloop.READ | self.ioloop.ERROR)
- self.ioloop.add_handler(self.p.stderr.fileno(), self.on_stderr,
- self.ioloop.READ | self.ioloop.ERROR)
-
- logging.info("#%s: Starting crawl for user %s (ip %s, fd%s, fd%s, fd%s).",
- self.id, self.username, self.request.remote_ip,
- self.p.stdin.fileno(), self.p.stdout.fileno(),
- self.p.stderr.fileno())
+ self.process = CrawlProcessHandler(games[game_id], self.username,
+ self.logger, self.ioloop)
+ self.process.end_callback = self._on_crawl_end
+ self.process.add_watcher(self)
+ self.process.start()
self.write_message("crawl_started();")
if dgl_mode:
- self.create_mock_ttyrec()
-
update_global_status()
- def stop_crawl(self):
- self.p.send_signal(subprocess.signal.SIGHUP)
- self.kill_timeout = self.ioloop.add_timeout(time.time() + kill_timeout, self.kill_crawl)
+ def _on_crawl_end(self):
+ self.process = None
- def kill_crawl(self):
- if self.p:
- logging.info("#%s: Killing crawl process after SIGHUP did nothing (user %s, ip %s).",
- self.id, self.username, self.request.remote_ip)
- self.p.send_signal(subprocess.signal.SIGTERM)
- self.kill_timeout = None
+ if self.client_terminated:
+ sockets.remove(self)
+ else:
+ if shutting_down:
+ self.close()
+ else:
+ # Go back to lobby
+ self.write_message("crawl_ended();")
+ if dgl_mode:
+ self.update_lobby()
+ else:
+ self.start_crawl(None)
+
+ if dgl_mode: update_global_status()
+
+ if shutting_down and len(sockets) == 0:
+ # The last crawl process has ended, now we can go
+ self.ioloop.stop()
def init_user(self):
with open("/dev/null", "w") as f:
@@ -292,72 +269,15 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler):
stdout = f, stderr = subprocess.STDOUT)
return result == 0
- def create_mock_ttyrec(self):
- now = datetime.datetime.utcnow()
- running_game_path = games[self.game_id]["running_game_path"]
- self.ttyrec_filename = os.path.join(running_game_path,
- self.username + ":" + now.strftime("%Y-%m-%d.%H:%M:%S")
- + ".ttyrec")
- f = open(self.ttyrec_filename, "w")
- f.close()
-
- def delete_mock_ttyrec(self):
- if self.ttyrec_filename:
- os.remove(self.ttyrec_filename)
- self.ttyrec_filename = None
-
- def check_where(self):
- morgue_path = games[self.game_id]["morgue_path"]
- wherefile = os.path.join(morgue_path, self.username, self.username + ".dglwhere")
- try:
- stat = os.stat(wherefile)
- if stat.st_mtime > self.wheretime:
- self.wheretime = time.time()
- f = open(wherefile, "r")
- _, _, newwhere = f.readline().partition("|")
- f.close()
-
- newwhere = newwhere.strip()
-
- if self.where != newwhere:
- self.where = newwhere
- update_global_status()
- except (OSError, IOError):
- pass
-
- def add_watcher(self, watcher):
- self.watchers.add(watcher)
- watcher.send_client(self.client_prefix)
- self.p.stdin.write("^r") # Redraw
- self.update_watcher_description()
-
- def remove_watcher(self, watcher):
- self.watchers.remove(watcher)
- self.update_watcher_description()
-
- def update_watcher_description(self):
- watcher_names = [watcher.username for watcher in self.watchers
- if watcher.username]
- anon_count = len(self.watchers) - len(watcher_names)
- s = ", ".join(watcher_names)
- if len(watcher_names) > 0 and anon_count > 0:
- s = s + ", and %i Anon" % anon_count
- elif anon_count > 0:
- s = "%i Anon" % anon_count
- self.write_message_all("watchers(%i,'%s')" %
- (len(self.watchers), s))
-
def stop_watching(self):
if self.watched_game:
- logging.info("#%s: User %s stopped watching %s #%s (ip: %s)",
- self.id, self.username, self.watched_game.username,
- self.watched_game.id, self.request.remote_ip)
+ self.logger.info("Stopped watching %s.", self.watched_game.username)
self.watched_game.remove_watcher(self)
self.watched_game = None
self.write_message("set_watching(false);")
self.write_message("go_lobby();")
- def send_client(self, client_prefix):
+ def set_client(self, game_params, client_prefix):
game_html = self.render_string(client_prefix + "/game.html", prefix = client_prefix)
self.write_message("delay_timeout = 1;$('#game').html(" +
tornado.escape.json_encode(game_html) + ");delay_ended();")
@@ -369,56 +289,17 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler):
tornado.escape.json_encode(msg) + ");")
self.close()
if self.is_running():
- self.stop_crawl()
-
- def poll_crawl(self):
- if self.p is not None and self.p.poll() is not None:
- self.ioloop.remove_handler(self.p.stdout.fileno())
- self.ioloop.remove_handler(self.p.stderr.fileno())
- self.p.stdout.close()
- self.p.stderr.close()
- self.p = None
-
- logging.info("#%s: Crawl terminated.", self.id)
-
- if self.kill_timeout:
- self.ioloop.remove_timeout(self.kill_timeout)
- self.kill_timeout = None
-
- if dgl_mode: self.delete_mock_ttyrec()
- self.where = None
-
- if self.client_terminated:
- global sockets
- sockets.remove(self)
- else:
- if shutting_down:
- self.close()
- else:
- # Go back to lobby
- self.write_message("crawl_ended();")
- if dgl_mode:
- self.update_lobby()
- else:
- self.start_crawl(None)
-
- if dgl_mode: update_global_status()
-
- for watcher in list(self.watchers):
- watcher.stop_watching()
-
- if shutting_down and len(sockets) == 0:
- # The last crawl process has ended, now we can go
- self.ioloop.stop()
+ self.process.stop()
def login(self, username):
self.username = username
+ self.logger.extra["username"] = username
if not self.init_user():
self.write_message("connection_closed('Could not initialize your rc and morgue!<br>" +
"This probably means there is something wrong with the server " +
"configuration.');")
- logging.warn("#%s: User initialization returned an error for user %s!",
- self.id, self.username)
+ self.logger.warning("User initialization returned an error for user %s!",
+ self.username)
self.username = None
self.close()
return
@@ -433,12 +314,10 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler):
username, _, password = message.partition(' ')
real_username = user_passwd_match(username, password)
if real_username:
- logging.info("#%s: User %s logged in from ip %s.",
- self.id, real_username, self.request.remote_ip)
+ self.logger.info("User %s logged in.", real_username)
self.login(real_username)
else:
- logging.warn("#%s: Failed login for user %s from ip %s.",
- self.id, username, self.request.remote_ip)
+ self.logger.warning("Failed login for user %s.", username)
self.write_message("login_failed();")
elif message.startswith("LoginToken: "):
@@ -447,12 +326,10 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler):
token = long(token)
if (token, username) in login_tokens:
del login_tokens[(token, username)]
- logging.info("#%s: User %s logged in from ip %s (via token).",
- self.id, username, self.request.remote_ip)
+ self.logger.info("User %s logged in (via token).", username)
self.login(username)
else:
- logging.warn("#%s: Wrong login token for user %s from ip %s.",
- self.id, username, self.request.remote_ip)
+ self.logger.warning("Wrong login token for user %s.", username)
self.write_message("login_failed();")
elif message == "Remember":
@@ -476,7 +353,7 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler):
return
elif message.startswith("Play: "):
- if self.p or self.watched_game: return
+ if self.process or self.watched_game: return
game_id = message[len("Play: "):]
self.start_crawl(game_id)
@@ -487,17 +364,15 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler):
self.update_lobby()
elif message.startswith("Watch: "):
- if self.p or self.watched_game: return
+ if self.process or self.watched_game: return
watch_username = message[len("Watch: "):]
- sockets = [socket for socket in find_user_sockets(watch_username)
- if socket.is_running()]
- if len(sockets) >= 1:
- socket = sockets[0]
- logging.info("#%s: User %s (ip: %s) started watching #%s %s.",
- self.id, self.username, self.request.remote_ip,
- socket.id, socket.username)
- self.watched_game = socket
- socket.add_watcher(self)
+ procs = [socket.process for socket in find_user_sockets(watch_username)
+ if socket.is_running()]
+ if len(procs) >= 1:
+ process = procs[0]
+ self.logger.info("Started watching %s.", process.username)
+ self.watched_game = process
+ process.add_watcher(self)
self.write_message("set_watching(true);")
else:
self.write_message("go_lobby();")
@@ -510,12 +385,12 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler):
chat_msg = ("<span class='chat_sender'>%s</span>: <span class='chat_msg'>%s</span>" %
(self.username, tornado.escape.xhtml_escape(message[len("Chat: "):])))
receiver = None
- if self.p:
- receiver = self
+ if self.process:
+ receiver = self.process
elif self.watched_game:
receiver = self.watched_game
if receiver:
- receiver.write_message_all("chat(%s);" % tornado.escape.json_encode(chat_msg))
+ receiver.send_to_all("chat(%s);" % tornado.escape.json_encode(chat_msg))
elif message.startswith("Register: "):
message = message[len("Register: "):]
@@ -523,18 +398,17 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler):
email, _, password = message.partition(" ")
error = register_user(username, password, email)
if error is None:
- logging.info("#%s: Registered user: %s (ip: %s)", self.id, username,
- self.request.remote_ip)
+ self.logger.info("Registered user %s.", username)
self.login(username)
else:
- logging.info("#%s: Registration attempt failed for username %s: %s (ip: %s)",
- self.id, username, error, self.request.remote_ip)
+ self.logger.info("Registration attempt failed for username %s: %s",
+ username, error)
self.write_message("register_failed(" +
tornado.escape.json_encode(error) + ");")
elif message == "GoLobby":
if self.is_running():
- self.stop_crawl()
+ self.process.stop()
elif self.watched_game:
self.stop_watching()
@@ -555,40 +429,32 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler):
f.write(codecs.BOM_UTF8)
f.write(contents.encode("utf8"))
- elif self.p is not None:
- if not message.startswith("^"):
- self.last_action_time = time.time()
+ elif message.startswith("{"):
+ self.process.handle_input(message)
- logging.debug("#%s: Message: %s (user: %s)", self.id, message, self.username)
- self.poll_crawl()
- if self.p is not None:
- self.p.stdin.write(message.encode("utf8"))
+ elif self.process:
+ self.logger.debug("Message: %s", message)
+ self.process.handle_input(message)
def write_message(self, msg):
try:
super(CrawlWebSocket, self).write_message(msg)
except:
- logging.warn("#%s: Exception trying to send message to %s.",
- self.id, self.request.remote_ip, exc_info = True)
+ self.logger.warning("Exception trying to send message.", exc_info = True)
self.ws_connection._abort()
- def write_message_all(self, msg):
+ def handle_message(self, msg):
if not self.client_terminated:
self.write_message(msg)
- for watcher in self.watchers:
- if not watcher.client_terminated:
- watcher.write_message(msg)
-
def on_close(self):
- global sockets
- if self.p is None and self in sockets:
+ if self.process is None and self in sockets:
sockets.remove(self)
if shutting_down and len(sockets) == 0:
# The last socket has been closed, now we can go
self.ioloop.stop()
elif self.is_running():
- self.stop_crawl()
+ self.process.stop()
if self.watched_game:
self.watched_game.remove_watcher(self)
@@ -596,49 +462,8 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler):
if self.timeout:
self.ioloop.remove_timeout(self.timeout)
- logging.info("#%s: Socket for ip %s closed.", self.id, self.request.remote_ip)
-
- def on_stderr(self, fd, events):
- if events & self.ioloop.ERROR:
- self.poll_crawl()
- elif events & self.ioloop.READ:
- s = self.p.stderr.readline()
-
- if self.client_terminated:
- return
-
- if not (s.isspace() or s == ""):
- logging.info("#%s: ERR: %s: %s",
- self.id, self.username, s.strip())
-
- self.poll_crawl()
-
- def handle_crawl_message(self, msg):
- if msg.startswith("^"):
- msg = msg[1:]
- if msg.startswith("ClientPrefix:"):
- msg = msg[len("ClientPrefix:"):]
- msg = msg.strip()
- self.client_prefix = msg
- logging.info("#%s: User %s is using client version %s.",
- self.id, self.username, self.client_prefix)
- self.send_client(self.client_prefix)
- else:
- self.write_message_all(msg)
-
- def on_stdout(self, fd, events):
- if events & self.ioloop.ERROR:
- self.poll_crawl()
- elif events & self.ioloop.READ:
- msg = self.p.stdout.readline()
-
- if self.client_terminated:
- return
-
- self.handle_crawl_message(msg)
+ self.logger.info("Socket closed.")
- self.poll_crawl()
- if dgl_mode: self.check_where()
def update_global_status():
update_all_lobbys()
@@ -654,14 +479,13 @@ def write_dgl_status_file():
f = open(dgl_status_file, "w")
for socket in list(sockets):
if socket.username and socket.is_running():
- f.write(socket.username + "#" +
- socket.game_id + "#" +
- (socket.where or "") + "#" +
- "0x0" + "#" +
- str(int(socket.idle_time())) + "#" +
- str(len(socket.watchers)) + "#\n")
+ f.write("%s#%s#%s#0x0#%s#%s#\n" %
+ (socket.username, socket.game_id,
+ (socket.process.where or ""),
+ str(int(socket.process.idle_time())),
+ str(socket.process.watcher_count())))
except (OSError, IOError) as e:
- logging.warn("Could not write dgl status file: %s", e)
+ logging.warning("Could not write dgl status file: %s", e)
finally:
if f: f.close()
diff --git a/crawl-ref/source/webserver/static/client.js b/crawl-ref/source/webserver/static/client.js
index f5c72ba515..86e55ab6c0 100644
--- a/crawl-ref/source/webserver/static/client.js
+++ b/crawl-ref/source/webserver/static/client.js
@@ -82,18 +82,32 @@ function handle_keypress(e)
{
focus_chat();
}
- else if (s == "\\")
+ else if (s == "{")
{
- socket.send("\\92\n");
- }
- else if (s == "^")
- {
- socket.send("\\94\n");
+ send_bytes(["{".charCodeAt(0)]);
}
else
socket.send(s);
}
+function send_keycode(code)
+{
+ socket.send('{"msg":"key","keycode":' + code + '}');
+}
+
+function send_bytes(bytes)
+{
+ s = '{"msg":"input","data":[';
+ $.each(bytes, function (i, code) {
+ if (i == 0)
+ s += code;
+ else
+ s += "," + code;
+ });
+ s += "]}";
+ socket.send(s);
+}
+
function handle_keydown(e)
{
if (current_layer == "lobby")
@@ -113,13 +127,13 @@ function handle_keydown(e)
if (e.which in ctrl_key_conversion)
{
e.preventDefault();
- socket.send("\\" + ctrl_key_conversion[e.which] + "\n");
+ send_keycode(ctrl_key_conversion[e.which]);
}
else if ($.inArray(String.fromCharCode(e.which), captured_control_keys) != -1)
{
e.preventDefault();
var code = e.which - "A".charCodeAt(0) + 1; // Compare the CONTROL macro in defines.h
- socket.send("\\" + code + "\n");
+ send_keycode(code);
}
}
else if (!e.ctrlKey && e.shiftKey && !e.altKey)
@@ -127,14 +141,15 @@ function handle_keydown(e)
if (e.which in shift_key_conversion)
{
e.preventDefault();
- socket.send("\\" + shift_key_conversion[e.which] + "\n");
+ send_keycode(shift_key_conversion[e.which]);
}
}
else if (!e.ctrlKey && !e.shiftKey && e.altKey)
{
- //e.preventDefault();
- //var s = String.fromCharCode(e.which);
- //socket.send("\\27\n" + s);
+ if (e.which == 18) return;
+
+ e.preventDefault();
+ send_bytes([27, e.which]);
}
else if (!e.ctrlKey && !e.shiftKey && !e.altKey)
{
@@ -146,7 +161,7 @@ function handle_keydown(e)
else if (e.which in key_conversion)
{
e.preventDefault();
- socket.send("\\" + key_conversion[e.which] + "\n");
+ send_keycode(key_conversion[e.which]);
}
else
log("Key: " + e.which);
diff --git a/crawl-ref/source/webserver/static/game/game.js b/crawl-ref/source/webserver/static/game/game.js
index e59364a082..eba2799bf7 100644
--- a/crawl-ref/source/webserver/static/game/game.js
+++ b/crawl-ref/source/webserver/static/game/game.js
@@ -168,20 +168,9 @@ function do_layout()
current_layout = layout;
- send_layout(layout);
return true;
}
-function send_layout(layout)
-{
- var msg = "";
- msg += "^s" + layout.stats_height + "\n";
- msg += "^W" + layout.crt_width + "\n";
- msg += "^H" + layout.crt_height + "\n";
- msg += "^m" + layout.msg_width + "\n";
- socket.send(msg);
-}
-
// View area -------------------------------------------------------------------
function vgrdc(x, y)
{
diff --git a/crawl-ref/source/webserver/templates/lobby.html b/crawl-ref/source/webserver/templates/lobby.html
index 3a268af3d2..29f33c72fd 100644
--- a/crawl-ref/source/webserver/templates/lobby.html
+++ b/crawl-ref/source/webserver/templates/lobby.html
@@ -6,8 +6,8 @@
</a>
</td>
<td>{{ running_game.game_id }}</td>
- <td>{{ (running_game.where or "") }}</td>
- <td>{{ int(running_game.idle_time()) }}s</td>
- <td>{{ len(running_game.watchers) }}</td>
+ <td>{{ (running_game.process.where or "") }}</td>
+ <td>{{ int(running_game.process.idle_time()) }}s</td>
+ <td>{{ running_game.process.watcher_count() }}</td>
</tr>
{% end %}
diff --git a/crawl-ref/source/webserver/terminal.py b/crawl-ref/source/webserver/terminal.py
new file mode 100644
index 0000000000..d26cf06a98
--- /dev/null
+++ b/crawl-ref/source/webserver/terminal.py
@@ -0,0 +1,127 @@
+import pty
+import termios
+import os
+import fcntl
+import struct
+import resource
+import sys
+import time
+
+BUFSIZ = 2048
+
+class TerminalRecorder(object):
+ def __init__(self, command, filename, io_loop):
+ self.io_loop = io_loop
+ self.command = command
+ self.ttyrec = open(filename, "w", 0)
+ self.id = id
+ self.returncode = None
+ self.output_buffer = ""
+
+ self.pid = None
+ self.child_fd = None
+
+ self.end_callback = None
+ self.output_callback = None
+
+ self._spawn()
+
+ def _spawn(self):
+ self.pid, self.child_fd = pty.fork()
+
+ if self.pid == 0:
+ # We're the child
+ # Set window size
+ cols = 80
+ lines = 24
+ s = struct.pack("HHHH", lines, cols, 0, 0)
+ fcntl.ioctl(sys.stdout.fileno(), termios.TIOCSWINSZ, s)
+
+ # Make sure not to retain any files from the parent
+ max_fd = resource.getrlimit(resource.RLIMIT_NOFILE)[0]
+ for i in range (3, max_fd):
+ try:
+ os.close (i)
+ except OSError:
+ pass
+
+ # And exec
+ env = dict(os.environ)
+ env["COLUMNS"] = str(cols)
+ env["LINES"] = str(lines)
+ env["TERM"] = "linux"
+ os.execvpe(self.command[0], self.command, env)
+
+ # We're the parent
+ self.io_loop.add_handler(self.child_fd,
+ self._handle_read,
+ self.io_loop.ERROR | self.io_loop.READ)
+
+ def _handle_read(self, fd, events):
+ if events & self.io_loop.READ:
+ buf = os.read(fd, BUFSIZ)
+
+ if len(buf) > 0:
+ self.write_ttyrec_chunk(buf)
+
+ self.output_buffer += buf
+ self._do_output_callback()
+
+ self.poll()
+
+ if events & self.io_loop.ERROR:
+ self.poll()
+
+ def write_ttyrec_header(self, sec, usec, l):
+ s = struct.pack("<iii", sec, usec, l)
+ self.ttyrec.write(s)
+
+ def write_ttyrec_chunk(self, data):
+ t = time.time()
+ self.write_ttyrec_header(int(t), int((t % 1) * 1000000), len(data))
+ self.ttyrec.write(data)
+
+ def _do_output_callback(self):
+ pos = self.output_buffer.find("\n")
+ while pos >= 0:
+ line = self.output_buffer[:pos]
+ self.output_buffer = self.output_buffer[pos + 1:]
+
+ if len(line) > 0:
+ if line[-1] == "\r": line = line[:-1]
+
+ if self.output_callback:
+ self.output_callback(line)
+
+ pos = self.output_buffer.find("\n")
+
+ def send_signal(self, signal):
+ os.kill(self.pid, signal)
+
+ def poll(self):
+ if self.returncode is None:
+ pid, status = os.waitpid(self.pid, os.WNOHANG)
+ if pid == self.pid:
+ if os.WIFSIGNALED(status):
+ self.returncode = -os.WTERMSIG(status)
+ elif os.WIFEXITED(status):
+ self.returncode = os.WEXITSTATUS(status)
+ else:
+ # Should never happen
+ raise RuntimeError("Unknown child exit status!")
+
+ if self.returncode is not None:
+ self.io_loop.remove_handler(self.child_fd)
+ os.close(self.child_fd)
+
+ self.ttyrec.close()
+
+ if self.end_callback:
+ self.end_callback()
+
+ return self.returncode
+
+ def write_input(self, data):
+ while len(data) > 0:
+ written = os.write(self.child_fd, data)
+ data = data[written:]