{ The Grendel Project - A Windows/Linux MUD Server Copyright (C) 2000-2004 by Michiel Rook Contact information: Webpage: http://www.grendelproject.nl/ E-Mail: michiel@grendelproject.nl Please observe the file "documentation\License.txt" before using this software. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * 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. Neither the name of The Grendel Project 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 COPYRIGHT HOLDERS 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. $Id: grendel.dpr,v 1.32 2004/04/14 21:56:46 druid Exp $ } program grendel; {$DESCRIPTION 'The Grendel Project - A Windows/Linux MUD Server. Copyright (c) 2000-2004 by Michiel Rook.'} {$R grendel_icon.res} {$DEFINE Grendel} {$IFDEF CONSOLEBUILD} {$APPTYPE CONSOLE} {$ENDIF} uses SysUtils, DateUtils, {$IFDEF WIN32} Windows, Winsock2, {$ENDIF} Classes, {$IFDEF LINUX} Libc, {$ENDIF} dtypes, player, mudsystem, socket, server, console, conns, fight, debug, constants; const pipeName : pchar = '\\.\pipe\grendel'; var oldExitProc : pointer; procedure sendtoall(const s : string); var iterator : GIterator; conn : GPlayerConnection; begin iterator := connection_list.iterator(); while (iterator.hasNext()) do begin conn := GPlayerConnection(iterator.next()); conn.send(s); end; iterator.Free(); end; procedure waitConnections(); begin // Wait for connection_list to clean itself while (connection_list.size() > 0) do begin Sleep(25); end; end; // Reboot procedure procedure rebootServer(); var {$IFDEF WIN32} SI: TStartupInfo; PI: TProcessInformation; {$ELSE} pid : cardinal; args : array[1..2] of PChar; {$ENDIF} begin {$IFDEF WIN32} FillChar(SI, SizeOf(SI), 0); SI.cb := SizeOf(SI); SI.wShowWindow := sw_show; if (not CreateProcess('grendel.exe',Nil, Nil, Nil, False, NORMAL_PRIORITY_CLASS or CREATE_NEW_CONSOLE, Nil, Nil, SI, PI)) then bugreport('reboot_mud', 'grendel.dpr', 'Could not execute grendel.exe, reboot failed!'); {$ELSE} pid := fork(); // fork succesful if (pid <> 0) then exit; args[1] := 'grendel'; args[2] := nil; execv('grendel', PPChar(@args[1])); {$ENDIF} end; // Copyover procedure procedure copyoverServer(); var {$IFDEF WIN32} SI: TStartupInfo; PI: TProcessInformation; pipe : THandle; prot : TWSAProtocol_Info; w, len : cardinal; name : array[0..1023] of char; {$ENDIF} {$IFDEF LINUX} output : TextFile; args : array[1..4] of PChar; fd : integer; {$ENDIF} node, node_next : GListNode; conn : GPlayerConnection; pid : cardinal; begin writeConsole('Server starting copyover...'); node := connection_list.head; while (node <> nil) do begin conn := GPlayerConnection(node.element); node_next := node.next; // disable MCCP compression conn.disableCompression(); if (conn.isPlaying()) then begin stopfighting(conn.ch); conn.ch.emptyBuffer; conn.send(#13#10'Slowly, you feel the world as you know it fading away in wisps of steam...'#13#10#13#10); end else begin conn.send(#13#10'This server is rebooting, please continue in a few minutes.'#13#10#13#10); conn.Terminate(); end; node := node_next; end; {$IFDEF WIN32} FillChar(SI, SizeOf(SI), 0); SI.cb := SizeOf(SI); SI.wShowWindow := sw_show; if (not CreateProcess('copyover.exe', nil, Nil, Nil, False, NORMAL_PRIORITY_CLASS or CREATE_NEW_CONSOLE, Nil, Nil, SI, PI)) then begin bugreport('copyover_mud', 'grendel.dpr', 'Could not execute copyover.exe, copyover failed!'); rebootServer(); end; pipe := CreateNamedPipe(pipeName, PIPE_ACCESS_DUPLEX, PIPE_WAIT or PIPE_TYPE_BYTE or PIPE_READMODE_BYTE, 10, 0, 0, 1000, nil); if (pipe = INVALID_HANDLE_VALUE) then begin writeConsole('Could not create pipe: ' + IntToStr(GetLastError())); exit; end; if (not ConnectNamedPipe(pipe, nil)) then begin bugreport('copyover_mud', 'grendel.dpr', 'Pipe did not initialize correctly!'); rebootServer(); end; pid := GetCurrentProcessID(); if (not WriteFile(pipe, pid, 4, w, nil)) then begin bugreport('copyover_mud', 'grendel.dpr', 'Broken pipe'); rebootServer(); end; node := connection_list.head; while (node <> nil) do begin conn := GPlayerConnection(node.element); node_next := node.next; conn.ch.save(conn.ch.name); if (WSADuplicateSocket(conn.socket.getDescriptor, PI.dwProcessId, @prot) = -1) then begin bugreport('copyover_mud', 'grendel.dpr', 'WSADuplicateSocket failed'); rebootServer(); end; if (not WriteFile(pipe, prot, sizeof(prot), w, nil)) then begin bugreport('copyover_mud', 'grendel.dpr', 'Broken pipe'); rebootServer(); end; strpcopy(name, conn.ch.name); len := strlen(name); if (not WriteFile(pipe, len, 4, w, nil)) then begin bugreport('copyover_mud', 'grendel.dpr', 'Broken pipe'); rebootServer(); end; if (not WriteFile(pipe, name, len, w, nil)) then begin bugreport('copyover_mud', 'grendel.dpr', 'Broken pipe'); rebootServer(); end; conn.Terminate(); node := node_next; end; waitConnections(); CloseHandle(pipe); {$ELSE} AssignFile(output, 'copyover.temp'); Rewrite(output); node := connection_list.head; while (node <> nil) do begin conn := GPlayerConnection(node.element); node_next := node.next; conn.ch.save(conn.ch.name); // duplicate socket desciptor fd := dup(conn.socket.getDescriptor); writeln(output, conn.ch.name); writeln(output, fd); writeln(output, conn.socket.getAddressFamily); conn.Terminate(); node := node_next; end; CloseFile(output); waitConnections(); if (FileExists('grendel.run')) then begin AssignFile(output, 'grendel.run'); {$I-} Erase(output); {$I+} end; args[1] := 'grendel'; args[2] := 'copyover'; args[3] := nil; args[4] := nil; pid := fork(); // fork succesful if (pid <> 0) then exit; execv('grendel', PPChar(@args[1])); {$ENDIF} end; // Recover from copyover procedure copyoverRecover(); var client_addr : TSockAddr_Storage; cl : PSockaddr; sk : GSocket; conn : GPlayerConnection; {$IFDEF WIN32} pipe : THandle; w, len : cardinal; prot : TWSAProtocol_Info; g : array[0..1023] of char; suc : boolean; sock : TSocket; l : integer; {$ELSE} input : TextFile; name : string; l : socklen_t; fd, af : integer; {$ENDIF} begin writeConsole('Recovering from copyover...'); {$IFDEF WIN32} pipe := INVALID_HANDLE_VALUE; while (true) do begin pipe := CreateFile(pipeName, GENERIC_READ or GENERIC_WRITE, 0, nil, OPEN_EXISTING, 0, 0); if (pipe <> INVALID_HANDLE_VALUE) then break; if (GetLastError() <> ERROR_PIPE_BUSY) then begin bugreport('copyoverRecover', 'grendel.dpr', 'Could not restart from copyover'); exit; end; // All pipe instances are busy, so wait a second if (not WaitNamedPipe(pipeName, 1000)) then begin bugreport('copyoverRecover', 'grendel.dpr', 'Could not restart from copyover'); exit; end; end; sock := -1; repeat suc := ReadFile(pipe, prot, sizeof(prot), w, nil); if (suc) then sock := WSASocket(prot.iAddressFamily, SOCK_STREAM, IPPROTO_IP, @prot, 0, 0); suc := ReadFile(pipe, len, 4, w, nil); if (not suc) then break; suc := ReadFile(pipe, g, len, w, nil); if (suc) and (sock <> -1) then begin g[len] := #0; cl := @client_addr; l := 128; getpeername(sock, cl^, l); sk := createSocket(prot.iAddressFamily, sock); sk.setNonBlocking(); sk.socketAddress := client_addr; sk.resolve(system_info.lookup_hosts); conn := GPlayerConnection.Create(sk, true, g); conn.Resume(); end; until (not suc); CloseHandle(pipe); {$ELSE} try AssignFile(input, 'copyover.temp'); Reset(input); while (not Eof(input)) do begin readln(input, name); readln(input, fd); readln(input, af); cl := @client_addr; l := 128; getpeername(fd, cl^, l); sk := createSocket(af, fd); sk.setNonBlocking(); sk.socketAddress := client_addr; sk.resolve(system_info.lookup_hosts); conn := GPlayerConnection.Create(sk, true, name); conn.Resume(); end; CloseFile(input); Erase(input); except on E : Exception do reportException(E, 'copyoverRecover()'); end; {$ENDIF} end; { Our last hope, the ExitProc handler } procedure serverExitProc; far; var f : TextFile; begin ExitProc := oldExitProc; {$I-} AssignFile(f, 'grendel.run'); Erase(f); {$I+} if (serverBooted) then begin sendtoall('------ GAME CRASH DETECTED! ---- Saving all players.'#13#10#13#10); sendtoall('The server should be back online in less than a minute.'#13#10); sendtoall('If the server doesn''t auto-reboot, please notify'#13#10); sendtoall(pchar('the administration, '+system_info.admin_email+'.'#13#10)); // save all characters and try to unlog before quitting flushConnections(); Sleep(1000); // give operator/logfile a message {$IFDEF CONSOLEBUILD} writeln('CRASH WARNING -- SERVER IS UNSTABLE, WILL TRY TO REBOOT'); {$ENDIF} rebootServer(); end; end; type GConsoleGrendel = class(GConsoleWriter) public procedure write(timestamp : integer; const text : string; debugLevel : integer = 0); override; end; procedure GConsoleGrendel.write(timestamp : integer; const text : string; debugLevel : integer = 0); begin writeln('[' + FormatDateTime('hh:nn:ss', UnixToDateTime(timestamp)) + '] ', text); end; var serverInstance : GServer; f : textfile; shutdownType : GServerShutdownTypes; tm : TDateTime; cons : GConsole; {$IFDEF WIN32} {$IFDEF CONSOLEBUILD} function controlHandler(event : DWORD) : boolean; begin Result := true; SetConsoleCtrlHandler(@controlHandler, false); serverInstance.shutdown(SHUTDOWNTYPE_HALT, 0); end; {$ENDIF} {$ENDIF} {$IFDEF LINUX} procedure handleSignal(signal : longint); cdecl; begin case signal of SIGTERM: begin writeConsole('Received SIGTERM, halting'); serverInstance.shutdown(SHUTDOWNTYPE_HALT, 0); end; end; end; procedure daemonize(); var sSet : TSigSet; aOld, aTerm, aHup : PSigAction; begin { block all signals except for SIGTERM/SIGHUP } sigfillset(sSet); sigdelset(sSet, SIGTERM); sigdelset(sSet, SIGHUP); sigprocmask(SIG_BLOCK, @sSet, nil); { setup the signal handlers } new(aOld); new(aHup); new(aTerm); aTerm^.__sigaction_handler := @handleSignal; aTerm^.sa_flags := 0; aTerm^.sa_restorer := nil; aHup^.__sigaction_handler:= @handleSignal; aHup^.sa_flags := 0; aHup^.sa_restorer := nil; SigAction(SIGTERM,aTerm,aOld); SigAction(SIGHUP,aHup,aOld); case fork() of 0: begin Close(input); { close standard in } AssignFile(output,'/dev/null'); ReWrite(output); AssignFile(ErrOutPut,'/dev/null'); ReWrite(ErrOutPut); end; -1: begin writeln('fork() failed, continuing on foreground'); end; else begin Halt(0); end; end; end; {$ENDIF} begin if (FileExists('grendel.run')) then begin {$IFDEF CONSOLEBUILD} writeln('Server is already running? (delete grendel.run if this is not the case)'); {$ELSE} {$IFDEF WIN32} MessageBox(0, 'Server is already running? (delete grendel.run if this is not the case)', 'Server is already running', 0); {$ENDIF} {$ENDIF} exit; end; oldExitProc := ExitProc; ExitProc := @serverExitProc; {$I-} AssignFile(f, 'grendel.run'); Rewrite(f); CloseFile(f); {$I+} cons := GConsole.Create(); cons.attachWriter(GConsoleLogWriter.Create('grendel')); {$IFDEF CONSOLEBUILD} cons.attachWriter(GConsoleGrendel.Create()); {$ENDIF} cons.Free(); {$IFDEF LINUX} sigignore(SIGPIPE); if (not FindCmdLineSwitch('f')) then daemonize(); {$ENDIF} initDebug(); {$IFDEF WIN32} {$IFDEF CONSOLEBUILD} SetConsoleCtrlHandler(@controlHandler, true); {$ENDIF} {$ENDIF} serverInstance := GServer.Create(); tm := Now(); serverInstance.init(); if (ParamStr(1) = 'copyover') then copyoverRecover(); tm := Now() - tm; writeConsole('Server boot took ' + FormatDateTime('s "second(s)," z "millisecond(s)"', tm)); writeConsole('Grendel ' + version_number + ' ready...'); shutdownType := serverInstance.gameLoop(); if (shutdownType = SHUTDOWNTYPE_COPYOVER) then begin copyoverServer(); end else begin flushConnections(); waitConnections(); end; serverInstance.cleanup(); serverInstance.Free(); if (shutdownType = SHUTDOWNTYPE_REBOOT) then rebootServer(); end.