Copyright 1996-2003 John D. Polstra.
* 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 acknowledgment:
* This product includes software developed by John D. Polstra.
* 4. The name of the author may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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: Main.m3.html,v 1.2 2009-06-26 16:35:25 wagner Exp $
MODULE Main;
IMPORT
ClientClass, CVProto, ErrMsg, File, FilePosix, FileWr, Fmt, FS, FSServer,
IO, IP, Logger, OSError, OSErrorPosix, Params, Pathname,
Process, RCSComp, RegularFile, Stdio, SupMisc, SysLogger, Text, Thread,
TimeStampLogger, TokScan, TreeComp, Unix, UnixMisc, Version, WrLogger;
CONST
ThreadStackSize = 16 * 1024; (* In units of Word.T *)
VAR
mainPid: Process.ID;
config := NEW(FSServer.Configuration);
PROCEDURE BecomeDaemon()
RAISES {OSError.E} =
BEGIN
WITH pid = Unix.fork() DO
IF pid = -1 THEN (* Error. *)
OSErrorPosix.Raise()
ELSIF pid # 0 THEN (* Parent process. *)
Unix.underscore_exit(0);
END;
END;
(* This is the child process. *)
IF UnixMisc.SetSID() = -1 THEN
OSErrorPosix.Raise();
END;
(* EVAL Unix.chdir(SupMisc.RootDir); FIXME - Not yet. *)
END BecomeDaemon;
PROCEDURE CheckPort(port: INTEGER): IP.Port =
BEGIN
IF port = IP.NullPort
OR NOT (FIRST(IP.Port) <= port AND port <= LAST(IP.Port)) THEN
ErrMsg.Fatal("Invalid port " & Fmt.Int(port));
END;
IF port < 1024 THEN
ErrMsg.Fatal("Reserved port " & Fmt.Int(port) & " not permitted");
END;
RETURN port;
END CheckPort;
PROCEDURE FetchArg(VAR argNum, argPos: CARDINAL): TEXT =
VAR
arg := Params.Get(argNum);
argLen := Text.Length(arg);
value: TEXT;
BEGIN
IF argPos < argLen THEN (* There was no whitespace after the flag. *)
value := Text.Sub(arg, argPos);
argPos := argLen;
ELSE (* The value is in a separate argument. *)
INC(argNum);
IF argNum >= Params.Count THEN
Usage();
END;
value := Params.Get(argNum);
END;
RETURN value;
END FetchArg;
PROCEDURE GoingDown() =
Called (hopefully) when process is killed or crashes.
BEGIN
IF config.logger # NIL AND Process.GetMyID() = mainPid THEN
Logger.Notice(config.logger, "Going down");
END;
END GoingDown;
PROCEDURE Redirect(in, out, err: File.T) =
BEGIN
IF in # NIL THEN EVAL Unix.dup2(in.fd, 0) END;
IF out # NIL THEN EVAL Unix.dup2(out.fd, 1) END;
IF err # NIL THEN EVAL Unix.dup2(err.fd, 2) END;
END Redirect;
PROCEDURE Usage(msg: TEXT := NIL) =
VAR
prog := Pathname.Last(Params.Get(0));
BEGIN
IF msg # NIL THEN
msg := msg & "\n";
ELSE
msg := "";
END;
ErrMsg.Fatal(msg
& "Usage: " & prog & " [options]"
& "\n Options:"
& "\n -A addr Listen for connections on a specific address"
& "\n -b base Base configuration directory (default " &
SupMisc.DefaultServerBase & ")"
& "\n -c collDirs Colon-separated search path for collections,"
& " relative"
& "\n to base (default \""
& SupMisc.DefaultServerCollDir & "\")"
& "\n -C maxClients Max. simultaneous clients (default 1 then exit)"
& "\n -e Don't close standard error and standard output"
& " files"
& "\n -f Don't become a daemon, even if -C is specified"
& "\n -l log Log file, or \"@facility\" (e.g., " &
"\"@local0\") to use syslog"
& "\n -p port Alternate TCP/IP port (default " &
Fmt.Int(SupMisc.Port) & ")"
& "\n -P range Range for server data port in passive mode"
& "\n (default unrestricted). Range can be a single"
& "\n port number, or a range \"lo-hi\"."
& "\n -s scanDir Directory where scan files can be found " &
"(default none)"
& "\n -v Print version and exit"
& "\n -x Require detailed compare for all RCS files"
& "\n -Z compLevel Compression level (0..9, default " &
Fmt.Int(SupMisc.DefaultCompression) & ")"
);
END Usage;
VAR
justPrintVersion := FALSE;
logName: Pathname.T := NIL;
logFile: File.T := NIL;
devNull, outFile : File.T := NIL;
noFork := FALSE;
keepStderr := FALSE;
argNum: CARDINAL;
argPos: CARDINAL;
argLen: CARDINAL;
option: CHAR;
arg: TEXT;
argVal: TEXT;
pos: INTEGER;
server: FSServer.T;
BEGIN
TRY
TRY
argNum := 1;
WHILE argNum < Params.Count DO
arg := Params.Get(argNum);
argLen := Text.Length(arg);
IF argLen > 0 AND Text.GetChar(arg, 0) = '-' THEN
argPos := 1;
WHILE argPos < argLen DO
option := Text.GetChar(arg, argPos);
INC(argPos);
CASE option OF
| 'A' =>
WITH t = FetchArg(argNum, argPos) DO
TRY
IF NOT SupMisc.ParseHost(t, config.localEndpoint.addr) THEN
ErrMsg.Fatal(t & ": host unknown");
END
EXCEPT IP.Error(list) =>
ErrMsg.Fatal(t & ": " & ErrMsg.StrError(list));
END;
END;
| 'b' =>
config.serverBase := FetchArg(argNum, argPos);
| 'c' =>
config.serverCollDirs := FetchArg(argNum, argPos);
| 'C' =>
config.maxChildren := TokScan.AtoI(FetchArg(argNum, argPos));
| 'd' =>
WITH level = TokScan.AtoI(FetchArg(argNum, argPos)) DO
ClientClass.debugLevel := level;
TreeComp.traceLevel := level;
RCSComp.traceLevel := level;
END;
| 'f' => noFork := TRUE;
| 'e' => keepStderr := TRUE;
| 'l' => logName := FetchArg(argNum, argPos);
| 'p' =>
config.localEndpoint.port :=
CheckPort(TokScan.AtoI(FetchArg(argNum, argPos)));
| 'P' =>
argVal := FetchArg(argNum, argPos);
pos := Text.FindChar(argVal, '-');
IF pos >= 0 THEN
config.loDataPort :=
CheckPort(TokScan.AtoI(Text.Sub(argVal, 0, pos),
"low data port"));
config.hiDataPort :=
CheckPort(TokScan.AtoI(Text.Sub(argVal, pos+1),
"high data port"));
IF config.loDataPort > config.hiDataPort THEN
ErrMsg.Fatal("Invalid data port range " & argVal);
END;
ELSE
config.loDataPort :=
CheckPort(TokScan.AtoI(argVal, "data port"));
config.hiDataPort := config.loDataPort;
END;
| 's' =>
config.serverScanDir := FetchArg(argNum, argPos);
| 't' =>
WITH level = TokScan.AtoI(FetchArg(argNum, argPos)) DO
TreeComp.traceLevel := level;
RCSComp.traceLevel := level;
END;
| 'v' => justPrintVersion := TRUE;
| 'x' => config.detailAllRCSFiles := TRUE;
| 'Z' =>
WITH val = TokScan.AtoI(FetchArg(argNum, argPos)) DO
IF 0 <= val AND val <= 9 THEN
config.compLevel := val;
ELSE
ErrMsg.Fatal("Compression level out of range 0..9");
END;
END;
ELSE
Usage("Invalid option \"-" & Text.FromChar(option) & "\"");
END;
END;
ELSE
Usage();
END;
INC(argNum);
END;
EXCEPT TokScan.Error(msg) =>
Usage(msg);
END;
IF justPrintVersion THEN
IO.Put("CVSup server\n");
IO.Put("Copyright 1996-2003 John D. Polstra\n");
IO.Put("Software version: " & Version.Name & "\n");
IO.Put("Protocol version: " & Fmt.Int(CVProto.Current.major) & "." &
Fmt.Int(CVProto.Current.minor) & "\n");
IO.Put("Operating system: " & Version.Target & "\n");
IO.Put("http://www.cvsup.org/\n");
IO.Put("Report problems to cvsup-bugs@polstra.com\n");
IO.Put("CVSup is a registered trademark of John D. Polstra\n");
Process.Exit(0);
END;
IF logName # NIL THEN
IF Text.Length(logName) > 0 AND Text.GetChar(logName, 0) = '@' THEN
(* Log to a syslog facility. *)
TRY
WITH facility = SysLogger.DecodeFacility(Text.Sub(logName, 1)) DO
config.logger := NEW(SysLogger.T).init("cvsupd", facility,
SysLogger.Options{SysLogger.Option.Pid});
END;
EXCEPT SysLogger.Error(msg) =>
ErrMsg.Fatal(msg);
END;
ELSE (* Log to a file. *)
TRY
logFile := FS.OpenFile(logName, truncate := FALSE);
TYPECASE logFile OF
| RegularFile.T(reg) =>
EVAL reg.seek(RegularFile.Origin.End, 0);
UnixMisc.AppendAlways(logFile);
ELSE (* Ignore *) END;
WITH logWr = NEW(FileWr.T).init(logFile) DO
config.logger := NEW(WrLogger.T).init(logWr);
config.logger := NEW(TimeStampLogger.T).init(config.logger);
END;
EXCEPT OSError.E(l) =>
ErrMsg.Fatal("Cannot create \"" & logName & "\"", l);
END;
END;
ELSIF config.maxChildren < 0 THEN (* Once-only mode. *)
config.logger := NEW(WrLogger.T).init(Stdio.stdout);
config.logger := NEW(TimeStampLogger.T).init(config.logger);
END;
(* Increase the default stack size for all threads. *)
Thread.MinDefaultStackSize(ThreadStackSize);
server := NEW(FSServer.T).init(config);
IF config.maxChildren >= 0 AND NOT noFork THEN (* Become a daemon. *)
TRY
BecomeDaemon();
(* Redirect stdin, stdout, and stderr. *)
devNull := FS.OpenFile(SupMisc.DevNull,
truncate := FALSE,
create := FS.CreateOption.Never);
TRY
IF logFile # NIL THEN (* Logging to a file *)
outFile := logFile;
ELSIF keepStderr THEN
outFile := NIL;
ELSE
outFile := devNull;
END;
Redirect(in := devNull, out := outFile, err := outFile);
FINALLY
IF devNull.fd > 2 THEN
devNull.close();
END;
END;
EXCEPT OSError.E(l) =>
ErrMsg.Fatal("Cannot become a daemon", l);
END;
END;
mainPid := Process.GetMyID();
Process.RegisterExitor(GoingDown);
server.run();
EXCEPT
| FSServer.Error(msg) =>
ErrMsg.Fatal(msg);
| Thread.Alerted =>
ErrMsg.Fatal("Interrupted");
END;
END Main.