dgamelaunch/ttyrec.c

692 lines
18 KiB
C

/*
* Copyright (c) 1980 Regents of the University of California.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. All advertising materials mentioning features or use of this software
* must display the following acknowledgement:
* This product includes software developed by the University of
* California, Berkeley and its contributors.
* 4. Neither the name of the University nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
/* 1999-02-22 Arkadiusz Mi¶kiewicz <misiek@misiek.eu.org>
* - added Native Language Support
*/
/* 2000-12-27 Satoru Takabayashi <satoru@namazu.org>
* - modify `script' to create `ttyrec'.
*/
/*
* script
*/
#include "dgamelaunch.h"
#include "config.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <termios.h>
#include <time.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#ifndef NOSTREAMS
# include <stropts.h>
#endif
#include <stdlib.h>
#include <stdint.h>
#include "ttyrec.h"
#include "io.h"
#ifndef XCASE
# define XCASE 0
#endif
static void query_encoding(int game, char *username);
int slave;
pid_t dgl_parent;
pid_t child, subchild;
pid_t input_child;
char* ipfile = NULL;
volatile int wait_for_menu = 0;
FILE *fscript;
int master;
struct termios tt;
struct winsize win;
char last_ttyrec[512] = { '\0' };
int ancient_encoding = 0;
void
ttyrec_id(int game, char *username, char *ttyrec_filename)
{
int i;
time_t tstamp;
Header h;
char *buf = (char *)malloc(1024);
char tmpbuf[256];
char *server_id = banner_var_value("$SERVERID");
if (!buf) return;
tstamp = time(NULL);
#define dCLRSCR "\033[2J"
#define dCRLF "\r\n"
snprintf(buf, 1024,
dCLRSCR "\033[1;1H" dCRLF
"Player: %s" dCRLF
"Game: %s" dCRLF
"Server: %s" dCRLF
"Filename: %s" dCRLF
"Time: (%lu) %s" dCRLF
dCLRSCR,
username,
myconfig[game]->game_name,
(server_id ? server_id : "Unknown"),
ttyrec_filename,
tstamp, ctime(&tstamp)
);
#undef dCLRSCR
#undef dCRLF
h.len = strlen(buf);
gettimeofday (&h.tv, NULL);
(void) write_header(fscript, &h);
(void) fwrite(buf, 1, h.len, fscript);
free(buf);
}
int
ttyrec_main (int game, char *username, char *ttyrec_path, char* ttyrec_filename)
{
char dirname[100];
/* Note our PID to let children kill the main dgl process for idling */
dgl_parent = getpid();
child = subchild = input_child = 0;
if (!ttyrec_path) {
child = fork();
if (child < 0) {
perror ("fork");
fail ();
}
if (child == 0) {
execvp (myconfig[game]->game_path, myconfig[game]->bin_args);
} else {
int status;
(void) wait(&status);
}
return 0;
}
if (ttyrec_path[strlen(ttyrec_path)-1] == '/')
snprintf (dirname, 100, "%s%s", ttyrec_path, ttyrec_filename);
else
snprintf (dirname, 100, "%s/%s", ttyrec_path, ttyrec_filename);
ancient_encoding = myconfig[game]->encoding;
if (ancient_encoding == -1)
query_encoding(game, username);
snprintf(last_ttyrec, 512, "%s", dirname);
atexit(&remove_ipfile);
if ((fscript = fopen (dirname, "w")) == NULL)
{
perror (dirname);
fail ();
}
setbuf (fscript, NULL);
fixtty ();
(void) signal (SIGCHLD, finish);
child = fork ();
if (child < 0)
{
perror ("fork");
fail ();
}
if (child == 0)
{
subchild = child = fork ();
if (child < 0)
{
perror ("fork");
fail ();
}
if (child)
{
close (slave);
ipfile = gen_inprogress_lock (game, child, ttyrec_filename);
ttyrec_id(game, username, ttyrec_filename);
dooutput (myconfig[game]->max_idle_time);
}
else
doshell (game, username);
}
(void) fclose (fscript);
wait_for_menu = 1;
input_child = fork();
if (input_child < 0)
{
perror ("fork2");
fail ();
}
if (!input_child)
doinput ();
else
{
while (wait_for_menu)
sleep(1);
}
remove_ipfile();
child = 0;
return 0;
}
void
doinput ()
{
register int cc;
char ibuf[BUFSIZ];
while ((cc = read (0, ibuf, BUFSIZ)) > 0)
(void) write (master, ibuf, cc);
done ();
}
void
finish (int sig)
{
int status;
register int pid;
register int die = 0;
(void)sig; /* unused */
while ((pid = wait3 (&status, WNOHANG, 0)) > 0)
{
if (pid == child)
die = 1;
}
if (die)
{
if (input_child)
{
// Need to kill the child that's writing input to pty.
kill(input_child, SIGTERM);
while ((pid = wait3(&status, WNOHANG, 0)) > 0);
}
else
done ();
}
wait_for_menu = 0;
}
void
game_idle_kill(int signal)
{
kill(child, SIGHUP);
kill(dgl_parent, SIGHUP);
}
static unsigned short charset_vt100[128] =
{
0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
0x0008, 0x0009, 0x000a, 0x000b, 0x000c, 0x000d, 0x000e, 0x000f,
0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017,
0x0018, 0x0019, 0x001a, 0x001b, 0x001c, 0x001d, 0x001e, 0x001f,
0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027,
0x0028, 0x0029, 0x002a, 0x2192, 0x2190, 0x2191, 0x2193, 0x002f,
0x2588, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x003a, 0x003b, 0x003c, 0x003d, 0x003e, 0x003f,
0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x004a, 0x004b, 0x004c, 0x004d, 0x004e, 0x004f,
0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
0x0058, 0x0059, 0x005a, 0x005b, 0x005c, 0x005d, 0x005e, 0x00a0,
#if 0
0x25c6, 0x2592, 0x2409, 0x240c, 0x240d, 0x240a, 0x00b0, 0x00b1,
0x2591, 0x240b, 0x2518, 0x2510, 0x250c, 0x2514, 0x253c, 0xf800,
0xf801, 0x2500, 0xf803, 0xf804, 0x251c, 0x2524, 0x2534, 0x252c,
0x2502, 0x2264, 0x2265, 0x03c0, 0x2260, 0x00a3, 0x00b7, 0x007f,
#endif
0x2666, 0x2592, 0x2409, 0x240c, 0x240d, 0x240a, 0x00b0, 0x00b1,
0x2424, 0x240b, 0x2518, 0x2510, 0x250c, 0x2514, 0x253c, 0x23ba,
0x23bb, 0x2500, 0x23bc, 0x23bd, 0x251c, 0x2524, 0x2534, 0x252c,
0x2502, 0x2264, 0x2265, 0x03c0, 0x2260, 0x00a3, 0x00b7, 0x0020,
};
static unsigned short charset_cp437[256] =
{
// Real IBM charset has no control codes, but they are needed by
// terminals.
0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
0x0008, 0x0009, 0x000a, 0x000b, 0x000c, 0x000d, 0x000e, 0x000f,
0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017,
0x0018, 0x0019, 0x001a, 0x001b, 0x001c, 0x001d, 0x001e, 0x001f,
#if 0
0x0000, 0x263a, 0x263b, 0x2665, 0x2666, 0x2663, 0x2660, 0x2022,
0x25d8, 0x25cb, 0x25d9, 0x2642, 0x2640, 0x266a, 0x266b, 0x263c,
0x25b6, 0x25c0, 0x2195, 0x203c, 0x00b6, 0x00a7, 0x25ac, 0x21a8,
0x2191, 0x2193, 0x2192, 0x2190, 0x221f, 0x2194, 0x25b2, 0x25bc,
#endif
0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027,
0x0028, 0x0029, 0x002a, 0x002b, 0x002c, 0x002d, 0x002e, 0x002f,
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x003a, 0x003b, 0x003c, 0x003d, 0x003e, 0x003f,
0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x004a, 0x004b, 0x004c, 0x004d, 0x004e, 0x004f,
0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
0x0058, 0x0059, 0x005a, 0x005b, 0x005c, 0x005d, 0x005e, 0x005f,
0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x006a, 0x006b, 0x006c, 0x006d, 0x006e, 0x006f,
0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
0x0078, 0x0079, 0x007a, 0x007b, 0x007c, 0x007d, 0x007e, 0x2302,
0x00c7, 0x00fc, 0x00e9, 0x00e2, 0x00e4, 0x00e0, 0x00e5, 0x00e7,
0x00ea, 0x00eb, 0x00e8, 0x00ef, 0x00ee, 0x00ec, 0x00c4, 0x00c5,
0x00c9, 0x00e6, 0x00c6, 0x00f4, 0x00f6, 0x00f2, 0x00fb, 0x00f9,
0x00ff, 0x00d6, 0x00dc, 0x00a2, 0x00a3, 0x00a5, 0x20a7, 0x0192,
0x00e1, 0x00ed, 0x00f3, 0x00fa, 0x00f1, 0x00d1, 0x00aa, 0x00ba,
0x00bf, 0x2310, 0x00ac, 0x00bd, 0x00bc, 0x00a1, 0x00ab, 0x00bb,
0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556,
0x2555, 0x2563, 0x2551, 0x2557, 0x255d, 0x255c, 0x255b, 0x2510,
0x2514, 0x2534, 0x252c, 0x251c, 0x2500, 0x253c, 0x255e, 0x255f,
0x255a, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256c, 0x2567,
0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256b,
0x256a, 0x2518, 0x250c, 0x2588, 0x2584, 0x258c, 0x2590, 0x2580,
0x03b1, 0x00df, 0x0393, 0x03c0, 0x03a3, 0x03c3, 0x00b5, 0x03c4,
0x03a6, 0x0398, 0x03a9, 0x03b4, 0x221e, 0x03c6, 0x03b5, 0x2229,
0x2261, 0x00b1, 0x2265, 0x2264, 0x2320, 0x2321, 0x00f7, 0x2248,
0x00b0, 0x2219, 0x00b7, 0x221a, 0x207f, 0x00b2, 0x25a0, 0x00a0,
};
// there must be at least 4 bytes free, NOT CHECKED!
static int wctoutf8(char *d, uint32_t s)
{
if (s < 0x80)
{
d[0] = s;
return 1;
}
if (s < 0x800)
{
d[0] = ( s >> 6) | 0xc0;
d[1] = ( s & 0x3f) | 0x80;
return 2;
}
if (s < 0x10000)
{
d[0] = ( s >> 12) | 0xe0;
d[1] = ((s >> 6) & 0x3f) | 0x80;
d[2] = ( s & 0x3f) | 0x80;
return 3;
}
if (s < 0x110000)
{
d[0] = ( s >> 18) | 0xf0;
d[1] = ((s >> 12) & 0x3f) | 0x80;
d[2] = ((s >> 6) & 0x3f) | 0x80;
d[3] = ( s & 0x3f) | 0x80;
return 4;
}
// Invalid char marker (U+FFFD).
d[0] = 0xef;
d[1] = 0xbf;
d[2] = 0xbd;
return 3;
}
void
dooutput (int max_idle_time)
{
int cc, i, len;
time_t tvec, time ();
char obuf[BUFSIZ], ubuf[BUFSIZ*4+2], *ctime (), *out;
int galt = 0; // vt100 G switch
setbuf (stdout, NULL);
(void) close (0);
tvec = time ((time_t *) NULL);
/* Set up SIGALRM handler to kill idle games */
signal(SIGALRM, game_idle_kill);
for (;;)
{
Header h;
cc = read (master, obuf, BUFSIZ);
if (cc <= 0)
break;
if (max_idle_time)
alarm(max_idle_time);
switch (ancient_encoding)
{
case 0: // UTF-8
default:
h.len = cc;
gettimeofday (&h.tv, NULL);
(void) write (1, obuf, cc);
(void) write_header (fscript, &h);
(void) fwrite (obuf, 1, cc, fscript);
break;
case 1: // IBM
out = ubuf;
// Old Crawl emits useless toggles of vt100 mode, even though it
// never uses them in this mode. And they break stuff...
for (i = 0; i < cc; i++)
{
if (galt == 2) // ignore "ESC ( 0", "ESC ( B" and anything such
{
galt = 0;
continue;
}
else if (galt == 1)
if (obuf[i] == '(')
{
galt = 2;
continue;
}
else
{
galt = 0; // false alarm, emit ESC and continue
*out ++ = 27;
}
else if (obuf[i] == 27)
{
galt = 1;
continue;
}
else if (obuf[i] == 14 || obuf[i] == 15)
continue;
out += wctoutf8(out, charset_cp437[(unsigned char)obuf[i]]);
}
h.len = len = out - ubuf;
gettimeofday(&h.tv, NULL);
write(1, ubuf, len);
write_header(fscript, &h);
fwrite(ubuf, 1, len, fscript);
break;
case 2: // DEC
out = ubuf;
for (i = 0; i < cc; i++)
{
if (obuf[i] == 14)
galt = 1;
else if (obuf[i] == 15)
galt = 0;
else if (obuf[i] & 0x80) // strictly 7-bit
out += wctoutf8(out, 0xFFFD); // or we could assume some other charset
else if (galt)
out += wctoutf8(out, charset_vt100[(int)obuf[i]]);
else
*out++ = obuf[i];
}
h.len = len = out - ubuf;
gettimeofday(&h.tv, NULL);
write(1, ubuf, len);
write_header(fscript, &h);
fwrite(ubuf, 1, len, fscript);
break;
}
}
done ();
}
void
doshell (int game, char *username)
{
getslave ();
(void) close (master);
(void) fclose (fscript);
(void) dup2 (slave, 0);
(void) dup2 (slave, 1);
(void) dup2 (slave, 2);
(void) close (slave);
/*
if (myconfig[game]->mkdir)
(void) mkdir(myconfig[game]->mkdir, 0755);
if (myconfig[game]->chdir)
(void) chdir(myconfig[game]->chdir);
*/
execvp (myconfig[game]->game_path, myconfig[game]->bin_args);
fail ();
}
void
fixtty ()
{
struct termios rtt;
rtt = tt;
rtt.c_iflag = 0;
rtt.c_lflag &= ~(ISIG | ICANON | XCASE | ECHO | ECHOE | ECHOK | ECHONL);
rtt.c_oflag = OPOST;
rtt.c_cc[VINTR] = _POSIX_VDISABLE;
rtt.c_cc[VQUIT] = _POSIX_VDISABLE;
rtt.c_cc[VERASE] = _POSIX_VDISABLE;
rtt.c_cc[VKILL] = _POSIX_VDISABLE;
rtt.c_cc[VEOF] = 1;
rtt.c_cc[VEOL] = 0;
rtt.c_cc[VMIN] = 1;
rtt.c_cc[VTIME] = 0;
(void) tcsetattr (0, TCSAFLUSH, &rtt);
}
void
fail ()
{
(void) kill (0, SIGTERM);
done ();
}
void
done ()
{
time_t tvec, time ();
char *ctime ();
if (subchild)
{
tvec = time ((time_t *) NULL);
(void) fclose (fscript);
(void) close (master);
}
else
{
(void) tcsetattr (0, TCSAFLUSH, &tt);
}
remove_ipfile();
graceful_exit (0);
}
void
getslave ()
{
(void) setsid ();
/* grantpt( master);
unlockpt(master);
if ((slave = open((const char *)ptsname(master), O_RDWR)) < 0) {
perror((const char *)ptsname(master));
fail();
perror("open(fd, O_RDWR)");
fail();
} */
#ifndef NOSTREAMS
if (isastream (slave))
{
if (ioctl (slave, I_PUSH, "ptem") < 0)
{
perror ("ioctl(fd, I_PUSH, ptem)");
fail ();
}
if (ioctl (slave, I_PUSH, "ldterm") < 0)
{
perror ("ioctl(fd, I_PUSH, ldterm)");
fail ();
}
#ifndef _HPUX_SOURCE
if (ioctl (slave, I_PUSH, "ttcompat") < 0)
{
perror ("ioctl(fd, I_PUSH, ttcompat)");
fail ();
}
#endif
}
#endif /* !NOSTREAMS */
(void) ioctl (0, TIOCGWINSZ, (char *) &win);
}
void
remove_ipfile (void)
{
if (ipfile != NULL) {
unlink (ipfile);
free(ipfile);
ipfile = NULL;
}
signal(SIGALRM, SIG_IGN);
}
int encoding_by_name(const char *enc)
{
if (!strcasecmp(enc, "UTF-8") || !strcasecmp(enc, "UNICODE"))
return 0;
if (!strcasecmp(enc, "IBM") || !strcasecmp(enc, "CP437"))
return 1;
if (!strcasecmp(enc, "DEC"))
return 2;
if (!strcasecmp(enc, "ASCII"))
return 1; // what to do with invalid chars?
return -1;
}
static void call_print_charset(char *exe, char **args)
{
char **args2, **a;
int nargs = 0;
for (args2 = args; *args2; args2++)
nargs++;
args2 = calloc(nargs + 1, sizeof(char*));
for (a = args2; *args; args++)
*a++ = *args;
*a++ = "--print-charset";
*a = 0;
execvp(exe, args2);
}
static void query_encoding(int game, char *username)
{
int son;
int p[2];
int null;
struct timeval tv;
fd_set se;
char buf[128];
if (pipe(p))
perror("pipe");
switch(son = fork())
{
case -1:
perror("fork");
fail();
case 0:
null = open("/dev/null", O_RDONLY);
if (null != -1)
dup2(null, 0), close(null);
else
{
fprintf(stderr, "Error: can't open /dev/null\n");
// non-fatal, but if the child opens some other file, it might
// get confused
close(0);
}
dup2(p[1], 1);
close(p[1]);
close(p[0]);
call_print_charset(myconfig[game]->game_path, myconfig[game]->bin_args);
exit(1);
}
close(p[1]);
FD_ZERO(&se);
FD_SET(p[0],&se);
tv.tv_sec = 60; // FIXME: a huge delay, in case there's a db rebuild
tv.tv_usec = 0;
if (select(p[0]+1,&se,NULL,NULL,&tv) != 1)
{
fprintf(stderr, "Error: can't obtain charset info.\nPress any key...\n");
read(0, buf, 1);
close(p[0]); // SIGPIPE
kill(son, SIGTERM); // and SIGTERM for a good measure
waitpid(son, 0, 0);
ancient_encoding = 0;
return;
}
// Sending _one_ short message over a pipe is atomic on all systems I know.
// Don't assume this on about any other file descriptor.
read(p[0], buf, sizeof(buf)-1);
buf[sizeof(buf)-1] = 0;
close(p[0]);
kill(son, SIGTERM);
waitpid(son, 0, 0);
if (strchr(buf, '\n'))
*strchr(buf, '\n') = 0;
ancient_encoding = encoding_by_name(buf);
if (ancient_encoding == -1)
{
fprintf(stderr, "Error: unknown encoding \"%s\"\nPress any key...\n", buf);
read(0, buf, 1);
}
}