/*
 * @(#)Server.cpp
 *
 * This file is part of webCDwriter - Network CD Writing.
 *
 * Copyright (C) 1999-2004 Jörg P. M. Haeger
 *
 * webCDwriter is free software. See CDWserver.cpp for details.
 *
 * Jörg Haeger, 21.04.1999
 */

#include <arpa/inet.h>
#include <assert.h>
#ifndef FreeBSD
#include <crypt.h>
#endif
#include <dirent.h>
#include <errno.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <unistd.h>
#include <utime.h>

#include "Accounts.h"
#include "Cdrdao.h"
#include "Cdrecord.h"
#include "Hints.h"
#include "InputStream.h"
#include "Support.h"
#include "Process.h"
#include "Version.h"
#include "Log.h"
#include "MP3.h"
#include "Config.h"
#include "Ogg.h"
#include "Queue.h"
#include "Session.h"
#include "Server.h"
#include "Socket.h"
#include "Store.h"
#include "Status.h"
#include "StringTokenizer.h"
#include "Wave.h"
#include "WriterQueues.h"

#ifdef PAM
#define CDWserver
#include "PAM.h"
#endif

/**
 * Create a new server process.
 *
 * @version 20040710
 * @author Jörg P. M. Haeger
 */
Server::Server(Socket &socket):
		socket(socket),
		inStream(socket.getInputStream2()) {
	time(&connectionT0);
	log.put(2, S.e + "Server::Server started "
		+ timeToString(connectionT0));

	pthread_mutex_lock(&activateConfigMutex);
	activeServersNum++;
	mainThreadID = Thread::currentThread();
	serverNo = nextServerNo++;
	pthread_mutex_unlock(&activateConfigMutex);

//	inStream = NULL;
	logFilesNum = 0;

	// states
	bytesReceived = 0;
	openCommands = 0;
	isOffline = 0;
	lineStrPending = 0;
	clientSystemName = clientSystemVersion = unknown;
	clientUserID = clientUserName = unknown;
	userID = userPassword = unknown;
	ejectBeforeVerify = true;

	// modes
	modeShowCdrecord = 0;
	modeShowMkisofs = 0;

	session = NULL;
	tmpSessionID = NULL;
	project = NULL;
	quit = 0;

	if (!socket.isConnected())
		return;

	// try to determine the requesting host
	clientIP = unknown;
	struct sockaddr_in peerName;
	socklen_t peerNameLen = sizeof peerName;
	if (getpeername(socket.getFD(), (struct sockaddr *)&peerName,
		&peerNameLen) == 0)
		if (peerName.sin_family == AF_INET) {
			clientIP = inet_ntoa(peerName.sin_addr);
			log.put(2, S.e + "peerName <" + clientIP
				+ "> port = " + peerName.sin_port);
		}

	// set the socket to SO_KEEPALIVE
	int on = 1;
	if (setsockopt(socket.getFD(), SOL_SOCKET, SO_KEEPALIVE,
		(void *)&on, sizeof on) < 0)
		log.put(1, S.e + "setsockopt SO_KEEPALIVE: " + strerror(errno));

//	inStream = socket.getInputStream2();
}

const char
	Server::errorInCommand[] = "error in command",
	Server::hint[] = "HINT",
	Server::hintPercentDone[] = "percentDone",
	Server::info[] = "INFO",
	Server::unknown[] = "unknown";

pthread_mutex_t Server::activateConfigMutex = PTHREAD_MUTEX_INITIALIZER;
bool Server::activateConfigPending = false;
int Server::activeServersNum = 0;
String Server::features = "CD-R, CD-RW";
String Server::license = "GNU General Public License";
int Server::maxServers = 1;
int Server::maxUsers = 10000;
String Server::serialNo = "-";
String Server::validity = "<i18n>Unlimited</i18n>";
pthread_mutex_t Server::licenseMutex = PTHREAD_MUTEX_INITIALIZER;
int Server::nextServerNo = 1;
WriterQueues *Server::writerQueue = NULL;
Writer *Server::writers = new Writer[maxNumOfWriters];

Server::~Server() {
	if (socket.isConnected())
		logConnection();

	if (session != NULL) {
		delete session;
		session = NULL;
	}
	if (tmpSessionID != NULL) {
		store->removeSession(tmpSessionID);
		delete[] tmpSessionID;
		tmpSessionID = NULL;
	}
	if (project != NULL) {
		fclose(project);
		project = NULL;
	}

	pthread_mutex_lock(&activateConfigMutex);
	activeServersNum--;
	pthread_mutex_unlock(&activateConfigMutex);
	if (activateConfigPending)
		Server::activateConfig();

	log.put(2, S.e + "Server::~Server terminated " + timeToString());
}

void Server::activateConfig() {
	pthread_mutex_lock(&activateConfigMutex);
	if (activeServersNum == 0) {
		config.reload();
		resetStatic();
		activateConfigPending = false;
	}
	else
		activateConfigPending = true;
	pthread_mutex_unlock(&activateConfigMutex);
}

void Server::addArgs(Process &p, const char *args) {
	p.addArgs(args);
}

void Server::blank(char *cmdStr) {
	int hasWriter = 0;
	try {
		writerQueue->add(serverNo, options.medium, options.speed);
		reserveDevice(serverNo);
		hasWriter = 1;
		strcat(cmdStr, " ");
		const char *modeStr;
		if (strstr(cmdStr, " all ") != NULL)
			modeStr = "all";
		else
		if (strstr(cmdStr, " fast ") != NULL)
			modeStr = "fast";
		else
		if (strstr(cmdStr, " session ") != NULL)
			modeStr = "session";
		else
			modeStr = "help";

		waitForDisc();
		readATIP();
		readTOC();

		toClient(hint, hintCDrecordStarted);
		run_cdrecordBlank(modeStr);

		writer().percent = 100;
		try { readTOC(); } catch (Exception *e2) { delete e2; }
		if (config.getEjectOnDone())
			eject();
		log.put("cdrecord success");
		successToClient("cdrecord success");

		writer().state = idle;
		writerQueue->remove(serverNo);
	}
	catch (Exception *e) {
		if (hasWriter) {
			if (!noDisc() && config.getEjectOnDone())
				eject();
			writer().state = idle;
		}
		writerQueue->remove(serverNo);
		throw e;
	}
}

void Server::burnCopy() {
	int hasReader = 0, hasWriter = 0;
	try {
		String CD = "CD";
		writerQueue->add(-serverNo, CD);
		reserveDevice(-serverNo);
		readerNo = writerNo();
		writers[readerNo].thread = NULL;
		hasReader = 1;
		ejectReader();

		writerQueue->add(serverNo, options.medium, options.speed);
		reserveDevice(serverNo);
		hasWriter = 1;
		time(&writer().burnT0);

		toClient(info, S.e
			+ "Please insert the master CD into the drive No. "
			+ (readerNo + 1)
			+ " (" + config.getWriterInfo(readerNo) + ")!");

		for (int i = 0; i < options.copies; i++) {
		waitForDisc();

		try {
			if (options.blankTOC) {
				toClient(hint, hintBlankingTOC);
				run_cdrecordBlank("fast");
			}

			toClient(hint, hintCDrecordStarted);
			writers[readerNo].state = reading;
			if (config.getCdrdao()) {
				run_cdrdaoCopy();
				if (options.verify && CDState().firstTrackIsData)
					verify();
			}
			else {
				run_cdrecordCopy();
				if (options.verify)
					verify();
			}
		}
		catch (Exception *e) {
			try { readTOC(); } catch (Exception *e2) { delete e2; }
			logSession("C", e->getMessage());
			throw e;
		}

		logSession("C", "OK");
		eject();
		if (options.copies > 1)
			log.put("cdrecord success (copy %d)", i + 1);
		else
			log.put("cdrecord success");
		}
		ejectReader();
		successToClient("cdrecord success");

		writer().state = idle;
		writerQueue->remove(serverNo);

		writers[readerNo].state = idle;
		writerQueue->remove(-serverNo);
	}
	catch (Exception *e) {
		if (hasWriter) {
			if (!noDisc())
				eject();
			writer().state = idle;
		}
		if (hasReader) {
			ejectReader();
			writerQueue->remove(serverNo);
			writers[readerNo].state = idle;
		}
		writerQueue->remove(-serverNo);
		throw e;
	}
}

void Server::burnSession() {
	// end of transfer
	time(&sessionT1);

	copiesReady = 0;
	copiesToDo = options.copies;

	if (session->getFirstTrack() == NULL)
		throw new Exception("empty session");

	Exception *exception = NULL;
	try {
		burnSession2(100 * serverNo);
	}
	catch (Exception *e) {
		exception = e;
	}

	if (exception != NULL)
		throw exception;

	successToClient("cdrecord success");
}

void Server::burnSession2(int threadID) {
	writerQueue->add(threadID, options.medium, options.speed);

	Exception *exception = NULL;
	try {
		reserveDevice(threadID);
		burnSession3();
	}
	catch (Exception *e) {
		exception = e;
	}
//	finally {
		writerQueue->remove(threadID);
//	}

	if (exception != NULL)
		throw exception;
}

void Server::burnSession3() {
	time(&writer().burnT0);

	Exception *exception = NULL;
	try {
		burnSession4();
	}
	catch (Exception *e) {
		exception = e;
	}
//	finally {
		if (!noDisc() && config.getEjectOnDone())
			eject();
		writer().state = idle;
		resetImageFile();
//	}

	if (exception != NULL)
		throw exception;
}

void Server::burnSession4() {
	int makeImage = session->getFirstTrack()->isDirectory();
	String type = "?";
	if (makeImage)
		type = "S";
	else
	if (session->getFirstTrack()->getName().endsWith(".wav"))
		type = "A";
	else
		type = "I";

	for (; copiesToDo > 0; copiesToDo--) {
		waitForDisc();

		try {
			if (CDState().mediaType.startsWith("DVD")) {
				run_growisofs();
				type = type + "D";
			}
			else {
//				readATIP();

				if (options.blankTOC) {
					toClient(hint, hintBlankingTOC);
					try {
						run_cdrecordBlank("fast");
					}
					catch (Exception *e) {
						if (CDState().isRW)
							throw e;
						else
							delete e;
					}
				}

				if (makeImage) {
					toClient(hint, hintMakingImage);
					run_mkisofs();
					toClient(hint, hintImageReady);
				}

				toClient(hint, hintCDrecordStarted);
				run_cdrecord();
			}

			if (options.verify)
				verify();
		}
		catch (Exception *e) {
			try { readTOC(); } catch (Exception *e2) { delete e2; }
			logSession(type.getBytes(), e->getMessage());
			log.error(S.e + e->getMessage().getBytes());
			throw e;
		}

		logSession(type.getBytes(), "OK");
//		try { readTOC(); } catch (Exception *e2) { delete e2; }
		if (config.getEjectOnDone())
			eject();
		if (options.copies > 1)
			log.put(3, S.e
				+ "cdrecord success (copy " + (++copiesReady) + ")");
		else
			log.put(3, S.e + "cdrecord success");
	}
}

void Server::checkFileName(const char *fileName) {
	int i = 0;
	while (1) {
		while (fileName[i] == '/')
			i++;

		if (fileName[i] == 0)
			break;

		if (fileName[i] == '.'
				&& fileName[i + 1] == '.'
				&& (fileName[i + 2] == 0
					|| fileName[i + 2] == '/'))
			throw new Exception("file name contains /../");

		while (fileName[i] != 0 && fileName[i] != '/')
			i++;
	}
}

void Server::checkLoggedIn() {
	if (userPassword == unknown)
		throw new Exception("Not logged in.");
}

void Server::checkOpenSession() {
	if (session == NULL)
		throw new Exception("no open session");
}

String *Server::dateToShortString() {
	time_t t;
	time(&t);
	struct tm *tm = localtime(&t);
	String *str = new String();
	*str = *str
		+ (1900 + tm->tm_year)
		+ (tm->tm_mon + 1 < 10? "0": "") + (tm->tm_mon + 1)
		+ (tm->tm_mday < 10? "0": "") + tm->tm_mday;
	return str;
}

void Server::eject2() {
	try {
		Cdrecord p(writerNo());
		p.addArg("-inq");
		p.addArg("-V");
		p.run();
		if (p.isTrayOpen())
			return;
	}
	catch (Exception *e) {
		delete e;
	}

	try {
		Cdrecord p(writerNo());
		p.addArg("-eject");
		p.run();
	}
	catch (Exception *e) {
		throw e;
	}
}

void Server::ejectReader() {
	try {
		Cdrecord p(readerNo);
		p.addArg("-eject");
		p.run();
	}
	catch (Exception *e) {
		throw e;
	}
}

void Server::errorToClient(const char *format, ...) {
	if (isOffline)
		return;
	if (openCommands == 0) {
		log.put("error: openCommands = %d", openCommands);
		return;
	}

	char str[8 * 1024];
	va_list ap;
	va_start(ap, format);
	vsnprintf(str, sizeof str - 1, format, ap);
	va_end(ap);

	// message to client is exactly one line
	int i = 0;
	while (str[i] != 0 && str[i] != '\n')
		i++;
	str[i++] = '\n';
	str[i] = 0;

	char lineStr[16 * 1024];
	sprintf(lineStr, "ERR %s", str);
	int n = socket.write(lineStr, strlen(lineStr));
	openCommands--;
}

void Server::errorToClient(String &str) {
	errorToClient("%s", str.getBytes());
	String::free(&str);
}

void Server::executeCmd() {
	openCommands++;

	char cmd[2 * sizeof lineStr],
		arg1[2 * sizeof lineStr],
		arg2[2 * sizeof lineStr],
		arg3[2 * sizeof lineStr],
		arg4[2 * sizeof lineStr],
		arg5[2 * sizeof lineStr];
	cmd[0] = arg1[0] = arg2[0] = arg3[0] = arg4[0] = arg5[0] = 0;

	int strsNum = sscanf(lineStr, "%s %s %s %s %s %s",
		cmd, arg1, arg2, arg3, arg4, arg5);
	removeEscChars(arg1);
	removeEscChars(arg2);
	removeEscChars(arg3);
	removeEscChars(arg4);
	removeEscChars(arg5);
	int argsNum = strsNum - 1;

	// write lineStr to the log
	if (strcasecmp(cmd, "changePassword") == 0
			|| strcasecmp(cmd, "login") == 0) {
		// don't write passwords to the log
		String msg = cmd;
		char *passwd = arg1;
		if (msg.equalsIgnoreCase("login")) {
			msg = msg + " " + arg1;
			passwd = arg2;
		}
		msg = msg + " xxxx";
#ifdef PAM
		if (!config.getActiveBoolean("PAM"))
#endif
			strcpy(passwd, &crypt(passwd, "JH")[2]);
		log.put(2, S.e + msg);
	}
	else
	if (strsNum > 0
		&& (strncasecmp(cmd, "putFile", 7) == 0
			|| strcasecmp(cmd, "mkdir") == 0))
		log.putv("command: <%s>", lineStr);
	else
		log.put("command: <%s>", lineStr);

	// anonymous mode commands
	if (userPassword == unknown
		&& !config.getAnonymousUse()
		&& strcasecmp(cmd, "client") != 0
		&& strcasecmp(cmd, "login") != 0
		&& strcasecmp(cmd, "newAccount") != 0
		&& strcasecmp(cmd, "setProtocol") != 0
		&& strcasecmp(cmd, "system") != 0
		&& strcasecmp(cmd, "user") != 0
		&& strcasecmp(cmd, "vm") != 0)
		throw new Exception("Anonymous use is not permitted.");

	// interpret the command
	if (project != NULL) {
		// save project mode commands
		if (strcasecmp(cmd, "add") == 0
			|| strcasecmp(cmd, "boot") == 0
			|| strcasecmp(cmd, "medium") == 0
			|| strcasecmp(cmd, "mkdir") == 0
			|| strcasecmp(cmd, "mv") == 0
			|| strcasecmp(cmd, "rm") == 0
			|| strcasecmp(cmd, "type") == 0) {
			fprintf(project, "%s\n", lineStr);
			successToClient("");
			return;
		}
		else
		if (strcasecmp(cmd, "list") == 0)
			;
		else {
			fclose(project);
			project = NULL;
		}
	}

	if (strlen(lineStr) == 0)
		throw new Exception("empty command");
	else
	if (strcasecmp(cmd, "blank") == 0)
		blank(lineStr);
	else
	if (strcasecmp(cmd, "burnCopy") == 0) {
		if (config.getCopySupport())
			burnCopy();
		else
			throw new Exception("not supported");
	}
	else
	if (strcasecmp(cmd, "burnSession") == 0) {
		checkOpenSession();
		try {
			// compatibility with protocol version 1
			if (strstr(lineStr, "multi") != NULL)
				options.closeCD = 0;
			if (strstr(lineStr, "offline") != NULL)
				options.burnOffline = 1;

			// extension for special environments
			if (argsNum == 1
				&& strcasecmp(arg1, "--reserveWriterOffline") == 0) {
				successToClient("reserving and burning offline");
				options.burnOffline = isOffline = 1;
			}

			burnSession();
			delete session;
			session = NULL;
		}
		catch (Exception *e) {
			delete session;
			session = NULL;
			throw e;
		}
	}
	else
	if (strcasecmp(cmd, "changePassword") == 0) {
		if (argsNum != 1)
			throw new Exception(errorInCommand);
		checkLoggedIn();
		accounts->changePassword(userID, arg1);
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "client") == 0) {
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "closeSession") == 0) {
//		checkOpenSession();
		if (session != NULL)
			delete session;
		session = NULL;
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "getSessionInfo") == 0) {
		if (strsNum == 2) {
			int files, tracks, size_KB;
			store->getInfo(userID, userPassword, arg1,
				files, tracks, size_KB);
			toClient(hint, "files %d", files);
			toClient(hint, "tracks %d", tracks);
			successToClient("");
		}
		else
			errorToClient("in command %s", cmd);
	}
	else
	if (strcasecmp(cmd, "linkFile") == 0) {
		if (argsNum < 2 || argsNum > 3)
			throw new Exception(errorInCommand);

		checkOpenSession();
		checkFileName(arg1);
		checkFileName(arg2);
		File file = new File(config.getActiveValue("exportDir"), arg1);
		if (!file.exists()) {
			if (file.getName().equals("MD5Verify.jar")) {
				file = new File(config.getDefaultValue("exportDir"), arg1);
				if (!file.exists())
					file = new File(
						config.getDefaultValue("binDir"), "MD5Verify.jar");
				if (!file.exists())
					throw new Exception(S.e + "The file " + file.getPath()
						+ " does not exist on the server");
			}
			else
				throw new Exception(S.e + "The file " + arg1
					+ " does not exist on the server");
		}

		session->linkFile(file, arg2);
		successToClient("file linked");
	}
	else
	if (strcasecmp(cmd, "linkTrack") == 0) {
		if (argsNum != 2)
			throw new Exception(errorInCommand);

		checkOpenSession();
		checkFileName(arg1);
		File track = new File(config.getActiveValue("exportDir"), arg1);
		if (!track.exists())
			throw new Exception(S.e + "Track " + arg1
				+ " does not exist on the server");

		session->linkTrack(track, arg2);
		successToClient("track linked");
	}
	else
	if (strcasecmp(cmd, "list") == 0) {
		char *dirName = arg1;
		if (argsNum != 1)
			throw new Exception(errorInCommand);

		checkFileName(dirName);
		File f = new File(config.getActiveValue("exportDir"), dirName);
		DIR *dir = opendir(f.getPath().getBytes());
		if (dir == NULL)
			throw new Exception("not a directory");

		struct dirent *dirEntry;
		while ((dirEntry = readdir(dir)) != NULL) {
			String name = dirEntry->d_name;
			if (name.equals(".") || name.equals(".."))
				continue;
			File f2(f, name.getBytes());
			String type = "";
			if (name.endsWith(".mp3") || name.endsWith(".MP3"))
				try {
					MP3 mp3(f2);
					type = type + " MP3 " + (int)mp3.getSeconds();
				}
				catch (Exception *e) {
					delete e;
				}
			else if (name.endsWith(".ogg") || name.endsWith(".OGG"))
				try {
					Ogg ogg(f2);
					type = type + " OGG " + (int)ogg.getSeconds();
				}
				catch (Exception *e) {
					delete e;
				}
			else if (name.endsWith(".wav") || name.endsWith(".WAV"))
				try {
					Wave wave(f2);
					type = type + " WAV " + (int)wave.getSeconds();
				}
				catch (Exception *e) {
					delete e;
				}

			const char *name2 = spaceToEsc(name.getBytes());
			toClient(hint, S.e
				+ (f2.isDirectory()? "d": "f")
				+ " " + f2.length()
				+ " " + name2
				+ type.getBytes());
			delete[] name2;
		}
		closedir(dir);
		successToClient("list done");
	}
	else
	if (strcasecmp(cmd, "listProjects") == 0) {
		if (argsNum != 0)
			throw new Exception(errorInCommand);

		File dir0(config.getProjectsDir(), userID);
		DIR *dir = opendir(dir0.getPath().getBytes());
		if (dir == NULL)
			throw new Exception(S.e + "Cannot open the directory "
				+ dir0.getPath().getBytes());

		struct dirent *dirEntry;
		while ((dirEntry = readdir(dir)) != NULL) {
			if (strcmp(dirEntry->d_name, ".") == 0
				|| strcmp(dirEntry->d_name, "..") == 0)
				continue;
			int files = 0, tracks = 0, size = 0;
			store->getInfo(userID, userPassword, dirEntry->d_name,
				files, tracks, size);
			const char *name = spaceToEsc(dirEntry->d_name);
			toClient(hint, "%s %d %d %d %d",
				name, files, tracks, size,
				File(dir0, dirEntry->d_name).lastModified());
			delete[] name;
		}
		closedir(dir);
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "listSessions") == 0) {
		const int max = 100;
		const char *IDs[max];
		int n = store->list(userID, userPassword, IDs, max);
		for (int i = 0; i < n; i++) {
			toClient(hint, "%s", IDs[i]);
			delete[] IDs[i];
		}
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "loadProject") == 0) {
		if (argsNum != 1)
			throw new Exception(errorInCommand);

		checkLoggedIn();

		if (project != NULL) {
			fclose(project);
			project = NULL;
		}

		char str[1024];
		snprintf(str, sizeof str, "%s/%s/%s",
			config.getProjectsDir(), userID, arg1);
		project = fopen(str, "r");
		if (project == NULL)
			throw new Exception(S.e + "Cannot open " + str);

		while (1) {
			if (fgets(str, sizeof str, project) == NULL)
				break;
			toClient(hint, "%s", str);
		}

		fclose(project);
		project = NULL;
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "login") == 0) {
		if (argsNum != 2)
			throw new Exception(errorInCommand);

		if (options.protocolVersion >= 3) {
#ifdef PAM
			if (config.getActiveBoolean("PAM")) {
				PAM4CDWserver::check(arg1, arg2);
				strcpy(arg2, &crypt(arg2, "JH")[2]);
			}
			else
#endif
				accounts->check(arg1, arg2);
			userID = strDup(arg1);
			userPassword = strDup(arg2);
		}
		successToClient("LOGIN completed");
	}
	else
	if (strcasecmp(cmd, "mkdir") == 0) {
		char *dirName = arg1;
		int mode, time;
		if (argsNum != 3
			|| sscanf(arg2, "%o", &mode) != 1
			|| sscanf(arg3, "%d", &time) != 1)
			throw new Exception(errorInCommand);

		checkOpenSession();
		checkFileName(dirName);

		session->makeDir(dirName, mode, time);
		successToClient("directory created");
	}
	else
	if (strcasecmp(cmd, "newAccount") == 0) {
		if (argsNum != 2)
			throw new Exception(errorInCommand);

		if (config.getActiveBoolean("managedAccounts"))
			throw new Exception(S.e + "Ask the admin for an account!");

		if (strcmp(arg1, "CDinfos") == 0
			|| strcmp(arg1, "archive") == 0
			|| strcmp(arg1, "bin") == 0
			|| strcmp(arg1, "config") == 0
			|| strcmp(arg1, "connects") == 0
			|| strcmp(arg1, "lineStr") == 0
			|| strcmp(arg1, "log") == 0
			|| strcmp(arg1, "projects") == 0
			|| strcmp(arg1, "root") == 0
			|| strcmp(arg1, "sessions") == 0
			|| strcmp(arg1, "vlog") == 0
			|| strstr(arg1, ".log") != NULL
			|| strstr(arg1, ".vlog") != NULL)
			throw new Exception("reserved user name");

		if (strstr(arg1, "-") != NULL
			|| strstr(arg1, ":") != NULL
			|| strstr(arg1, "/") != NULL
			|| strstr(arg1, "\\") != NULL
			|| strstr(arg1, "..") != NULL)
			throw new Exception("invalid user name");

		char passwd[8 + 1];
		time_t t;
		time(&t);
		srandom(t);
		for (int i = 0; i < sizeof passwd - 1; i++)
			passwd[i] = 'A' + random() % ('Z' - 'A' + 1);
		passwd[sizeof passwd - 1] = 0;

		accounts->add(arg1, &crypt(passwd, "JH")[2], arg2);

		char cmdStr[4 * 1024];
		snprintf(cmdStr, sizeof cmdStr,
			"echo User: %s, Password: %s | mail -s %s %s",
			arg1, passwd,
			"\"webCDwriter Account\"", arg2);
		if (system(cmdStr) != 0) {
			accounts->remove(arg1);
			throw new Exception(S.e + "Sending an eMail to "
				+ arg2 + " failed.");
		}

		successToClient("added");
	}
	else
	if (strcasecmp(cmd, "newSession") == 0
		|| strcasecmp(cmd, "openSession") == 0) {
		if (argsNum > 1)
			throw new Exception(errorInCommand);

		if (argsNum == 0)
			snprintf(arg1, sizeof arg1, "%04d", serverNo);

		char userID2[strlen(userID) + 1 + 1];
		if (userPassword == unknown) {
			sprintf(userID2, ":%s", userID);
			snprintf(arg1, sizeof arg1, "%04d", serverNo);
		}
		else
			strcpy(userID2, userID);

		if (session != NULL) {
			delete session;
			session = NULL;
		}
		session = store->createSession(this, userID2, "", arg1,
			strcasecmp(cmd, "newSession") == 0);

		// delete a different previous temporary session
		if (tmpSessionID != NULL
			&& strcmp(tmpSessionID, session->getID()) != 0) {
			store->removeSession(tmpSessionID);
			delete[] tmpSessionID;
			tmpSessionID = NULL;
		}

		// store the ID of a temporary session
		if (userPassword == unknown || argsNum == 0)
			tmpSessionID = strDup(session->getID());

		if (strcasecmp(cmd, "openSession") == 0) {
			toClient(hint, "files %d",
				session->getFilesNum());
			toClient(hint, "tracks %d",
				session->getTracksNum());
		}

		options.reset();
		if (argsNum > 0)
			options.volumeID = arg1;
		successToClient("");
		time(&sessionT0);
	}
	else
	if (strcasecmp(cmd, "noop") == 0)
		successToClient("");
	else
	if (strcasecmp(cmd, "putFile") == 0) {
		char *fileName = arg1;
		int length, mode, time;
		if (argsNum != 4
			|| sscanf(arg2, "%d", &length) != 1
			|| sscanf(arg3, "%o", &mode) != 1
			|| sscanf(arg4, "%d", &time) != 1)
			throw new Exception(errorInCommand);

		bytesReceived += length;
		try {
			checkOpenSession();
			checkFileName(fileName);
		}
		catch (Exception *e) {
			inStream.skip(length);
			throw e;
		}

		session->putFile(inStream, fileName, length, mode, time);
		successToClient("file stored");
	}
	else
	if (strcasecmp(cmd, "putFileContinue") == 0) {
		int length;
		if (argsNum != 1
			|| sscanf(arg1, "%d", &length) != 1)
			throw new Exception(errorInCommand);

		bytesReceived += length;
		try {
			checkOpenSession();
		}
		catch (Exception *e) {
			inStream.skip(length);
			throw e;
		}

		putFileContinue(inStream, length);
	}
	else
	if (strcasecmp(cmd, "putFileEnd") == 0) {
		int mode, time;
		if (argsNum != 2
			|| sscanf(arg1, "%o", &mode) != 1
			|| sscanf(arg2, "%d", &time) != 1)
			throw new Exception(errorInCommand);

		checkOpenSession();
		putFileEnd(mode, time);
		successToClient("file closed");
	}
	else
	if (strcasecmp(cmd, "putFileStart") == 0) {
		if (argsNum != 1 && argsNum != 2)
			throw new Exception(errorInCommand);

		const char *fileName = arg1;
		checkOpenSession();
		checkFileName(fileName);
		putFileStart(fileName, arg2);
		successToClient("file created");
	}
	else
	if (strcasecmp(cmd, "putTrack") == 0) {
		char *trackType = arg1;
		int length;
		if (argsNum != 2
			|| sscanf(arg2, "%d", &length) != 1)
			throw new Exception(errorInCommand);

		bytesReceived += length;
		try {
			checkOpenSession();
			if (strcasecmp(trackType, "wav") == 0
				|| strcasecmp(trackType, "mp3") == 0
				|| strcasecmp(trackType, "ogg") == 0)
				if (!config.getActiveBoolean("audioCDSupport"))
					throw new Exception("no audio CD support");
			if (strcasecmp(trackType, "mp3") == 0
					&& !config.getActiveBoolean("MP3decoding"))
				throw new Exception("no MP3 decoding");
			if (strcasecmp(trackType, "ogg") == 0
					&& !config.getActiveBoolean("oggDecoding"))
				throw new Exception("no ogg decoding");
		}
		catch (Exception *e) {
			inStream.skip(length);
			throw e;
		}

		if (strcmp(trackType, "continue") != 0) {
			session->newTrack(trackType);
			if (strcmp(trackType, "inf") == 0)
				options.CDText = 1;
		}
		session->putTrack(inStream, length);
		successToClient("file stored");
	}
	else
	if (strcasecmp(cmd, "quit") == 0) {
		log.put("connection terminated by client");
		successToClient("");
		quit = 1;
	}
	else
	if (strcasecmp(cmd, "remove") == 0) {
		if (argsNum != 1)
			throw new Exception(errorInCommand);

		char *fileName = arg1;
		checkOpenSession();
		checkFileName(fileName);
		session->remove(fileName);
		successToClient("file removed");
	}
	else
	if (strcasecmp(cmd, "resetOptions") == 0) {
		options.reset();
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "saveProject") == 0) {
		if (argsNum != 1)
			throw new Exception(errorInCommand);

		checkLoggedIn();

		if (project != NULL) {
			fclose(project);
			project = NULL;
		}

		char str[1024];
		snprintf(str, sizeof str, "%s/%s/%s",
			config.getProjectsDir(), userID, arg1);
		makeDirs(str);
		project = fopen(str, "w");
		if (project == NULL)
			throw new Exception(S.e + "Cannot open " + str);

		log.put("project %s started", str);
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "setAcceptEmptyCD") == 0) {
		if (openCommands > 0)
			openCommands--;
	}
	else
	if (strcasecmp(cmd, "setAcceptKey") == 0) {
		if (strsNum != 2)
			throw new Exception("setAcceptKey <key>");
		if (strlen(arg1) > 32)
			arg1[32] = 0;
		options.acceptKey = arg1;
		options.writeProtection = arg1;
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "setAcceptKey2") == 0) {
		if (openCommands > 0)
			openCommands--;
	}
	else
	if (strcasecmp(cmd, "setAcceptMedium") == 0) {
		if (openCommands > 0)
			openCommands--;
	}
	else
	if (strcasecmp(cmd, "setAcceptWithoutUserID") == 0) {
		if (openCommands > 0)
			openCommands--;
	}
	else
	if (strcasecmp(cmd, "setBlankTOC") == 0) {
		options.blankTOC = 1;
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "setBootImage") == 0) {
		if (strsNum != 2)
			throw new Exception("setBootImage <filename>");
		checkFileName(arg1);
		char *ptr = arg1;
		while (*ptr == '/')
			ptr++;
		options.bootImage = ptr;
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "setBurnOffline") == 0) {
		options.burnOffline = 1;
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "setCloseCD") == 0) {
		options.closeCD = 1;
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "setCopies") == 0) {
		if (strsNum == 2
				&& sscanf(arg1, "%d", &options.copies) == 1
				&& options.copies > 0
				&& options.copies <= config.getMaxCopies())
			successToClient("");
		else
			errorToClient("missing or invalid argument");
	}
	else
	if (strcasecmp(cmd, "setDAO") == 0) {
		options.DAO = 1;
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "setDVDVideo") == 0) {
		options.DVDVideo = 1;
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "setFormat") == 0) {
		options.format = 1;
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "setHFS") == 0) {
		options.HFS = 1;
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "setHideContents") == 0) {
		options.hideContents = 1;
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "setJoliet") == 0) {
		options.joliet = 1;
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "setLaserOff") == 0) {
		options.laserOff = 1;
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "setMedium") == 0) {
		if (argsNum != 1)
			throw new Exception(S.e
				+ "setMedium [CD-R | CD-RW"
				+ " | DVD-RW | DVD-R | DVD+RW | DVD+R]");
		String medium = arg1;
		if (medium.equals("CD-R")
				|| medium.equals("CD-RW"))
			;
		else
			checkMediumSupport(medium.getBytes());
		options.medium = arg1;
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "setOverburn") == 0) {
		options.overburn = config.getActiveBoolean("overburnSupport");
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "setProjectInfo") == 0) {
		if (argsNum != 4)
			throw new Exception(S.e
				+ "setProjectInfo <from> <to> <label> <contents>");
		options.projectInfo.from = arg1;
		options.projectInfo.to = arg2;
		options.projectInfo.label = arg3;
		options.projectInfo.contents = arg4;
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "setProtocol") == 0) {
		if (strsNum == 2
			&& sscanf(arg1, "%d",
				&options.protocolVersion) == 1) {
			if (options.protocolVersion >= 2) {
				modeShowCdrecord = 1;
				modeShowMkisofs = 1;
			}
			successToClient("");
		}
		else
			throw new Exception("invalid");
	}
	else
	if (strcasecmp(cmd, "setRejectEmptyCD") == 0) {
		options.rejectEmptyCD = 1;
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "setRejectWithoutUserID") == 0) {
		options.rejectUnprotectedCD = 1;
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "setRockRidge") == 0) {
		options.rockRidge = 1;
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "setShowCdrecord") == 0) {
		modeShowCdrecord = 1;
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "setShowMkisofs") == 0) {
		modeShowMkisofs = 1;
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "setSpeed") == 0) {
		int n = 0;
		if (strsNum == 2
				&& sscanf(arg1, "%d", &n) == 1) {
			options.speed = min(n, config.getMaxSpeed());
			successToClient("");
		}
		else
			errorToClient("missing or invalid argument");
	}
	else
	if (strcasecmp(cmd, "setVerify") == 0) {
		if (config.getVerify() > 0) {
			options.verify = 1;
			successToClient("");
		}
		else
			errorToClient("not supported");
	}
	else
	if (strcasecmp(cmd, "setVolumeID") == 0) {
		if (strsNum != 2)
			throw new Exception("setVolumeID <ID>");
		if (strlen(arg1) > 32)
			arg1[32] = 0;
		options.volumeID = arg1;
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "setWaitForEmptyCD") == 0) {
		options.acceptKey = "";
		options.rejectUnprotectedCD = 1;
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "setWriteProtection") == 0) {
		if (strsNum != 2)
			throw new Exception("setWriteProtection <key>");
		if (strlen(arg1) > 32)
			arg1[32] = 0;
		options.writeProtection = arg1;
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "stat") == 0) {
		if (argsNum != 1)
			throw new Exception(errorInCommand);
		String name = arg1;

		checkFileName(name.getBytes());
		File f = new File(
			config.getActiveValue("exportDir"), name.getBytes());
		if (!f.exists())
			throw new Exception("does not exist");

			String type = "";
			if (f.isDirectory()) {
				int filesCounter = 0;
				int KB = f.getTreeSizeKB(filesCounter);
				KB += 1000 * 1000 * 1000;
				type = type + " treeSizeKB " + filesCounter + KB;
			}
			else
			if (name.endsWith(".mp3") || name.endsWith(".MP3"))
				try {
					MP3 mp3(f);
					type = type + " MP3 " + (int)mp3.getSeconds();
				}
				catch (Exception *e) {
					delete e;
				}
			else if (name.endsWith(".ogg") || name.endsWith(".OGG"))
				try {
					Ogg ogg(f);
					type = type + " OGG " + (int)ogg.getSeconds();
				}
				catch (Exception *e) {
					delete e;
				}
			else if (name.endsWith(".wav") || name.endsWith(".WAV"))
				try {
					Wave wave(f);
					type = type + " WAV " + (int)wave.getSeconds();
				}
				catch (Exception *e) {
					delete e;
				}

		toClient(hint, S.e
			+ (f.isDirectory()? "d": "f")
			+ " " + f.length()
			+ type.getBytes());
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "system") == 0) {
		if (strsNum >= 2)
			clientSystemName = strDup(arg1);
		if (strsNum >= 3)
			clientSystemVersion = strDup(arg2);
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "user") == 0) {
		if (strsNum >= 2 && strcasecmp(arg1, "noID") != 0)
			clientUserID = strDup(arg1);
		if (strsNum >= 3)
			clientUserName = strDup(arg2);
		if (userID == unknown) {
			char str[1024];
			if (clientUserID == unknown)
				snprintf(str, sizeof str, "%s-%s",
					clientSystemName, clientUserName);
			else
				snprintf(str, sizeof str, "%s-%s-%s",
					clientSystemName, clientUserID, clientUserName);
			userID = strDup(str);
		}
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "vm") == 0) {
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "cancel") == 0) {
		if (openCommands > 0)
			openCommands--;
	}
	else {
		saveLineStr();
		throw new Exception("unknown command");
	}
}

bool Server::filterCancel(bool cancel) {
	String line = "";
		if (!cancel)
			// no input available
			return cancel;
		if (inStream.readLine(lineStr, sizeof lineStr) == NULL)
			return true;

		line = lineStr;
		lineStrPending = 1;

	StringTokenizer tokenizer(line.getBytes(), " \t\n");
	String cmd = "", arg1 = "", arg2 = "";
	try {
		cmd = tokenizer.nextToken();
		if (cmd.startsWith("W")) {
			String str = cmd.substring(1);
			if (str.length() > 0) {
				try {
					int no = Integer::getInt(str.getBytes());
					cmd = tokenizer.nextToken();
				}
				catch (Exception *e) {
					delete e;
				}
			}
		}
		arg1 = tokenizer.nextToken();
		arg2 = tokenizer.nextToken();
	}
	catch (NoSuchElementException *e) {
		delete e;
	}

	if (cmd.equalsIgnoreCase("setBurnOffline")) {
		options.burnOffline = isOffline = 1;
		cancel = false;
		log.put(3, "Server::filterCancel: setBurnOffline");
	}
	else
	if (cmd.equalsIgnoreCase("setAcceptEmptyCD")) {
		options.rejectEmptyCD = false;
		lineStrPending = 0;
		cancel = false;
		log.put(3, S.e + "Server::filterCancel: setAcceptEmptyCD");
		load();
	}
	else
	if (cmd.equalsIgnoreCase("setAcceptKey")
			|| cmd.equalsIgnoreCase("setAcceptKey2")) {
		if (arg1.length() == 0)
			arg1 = "default";
		if (arg1.length() > 32)
			arg1 = arg1.substring(0, 32);
		options.acceptKey = arg1;
		lineStrPending = 0;
		cancel = false;
		log.put(3, S.e + "Server::filterCancel: setAcceptKey2"
			+ " <" + arg1 + ">");
		load();
	}
	else
	if (cmd.equalsIgnoreCase("setAcceptMedium")) {
		options.medium = CDState().mediaType;
		lineStrPending = 0;
		cancel = false;
		log.put(3, S.e + "Server::filterCancel: setAcceptMedium");
		load();
	}
	else
	if (cmd.equalsIgnoreCase("setAcceptWithoutUserID")) {
		options.rejectUnprotectedCD = false;
		lineStrPending = 0;
		cancel = false;
		log.put(3, S.e + "Server::filterCancel: setAcceptWithoutUserID");
		load();
	}

	return cancel;
}

String *Server::getNewLogFileName() {
	logFilesNum++;
	String str = "";
	str = str
		+ config.getLogDir()
		+ "/" + userID
		+ "/" + timeToDigits(connectionT0)
		+ "."
			+ (logFilesNum < 100? "0": "")
			+ (logFilesNum < 10? "0": "")
			+ logFilesNum;
	return new String(str);
}

int Server::hextoint(const char *str, int length) {
	int n = 0;
	for (int i = 0; i < length; i++) {
		int c = str[i], d = 0;
		if ('0' <= c && c <= '9')
			d = c - '0';
		else if ('A' <= c && c <= 'F')
			d = c - 'A' + 10;
		else if ('a' <= c && c <= 'f')
			d = c - 'a' + 10;
		else
			return -1;
		n = 16 * n + d;
	}
	return n;
}

void Server::initStatic() {
	accounts = NULL;
	store = NULL;
	config.reload();
	resetStatic();
	status.start();
}

bool Server::isCommandCancelled() {
	if (inStream.waitForInput(0) == 0)
		return false;

	if (inStream.readLine(lineStr, sizeof lineStr) != NULL) {
		String cmd = this->lineStr;
		if (cmd.length() > 6)
			cmd = cmd.substring(0, 6);
		else
			log.put(5, S.e + "* isCommandCancelled: <" + cmd + ">");
		if (cmd.equalsIgnoreCase("cancel"))
			return true;
		else
			lineStrPending = 1;
	}
	return false;
}

bool Server::isMediumOK() {
	log.put(5, "Server::isMediumOK()");
	if (!options.rejectUnprotectedCD)
		return true;
//	toClient(hint, S.e + "Medium detected. Reading TOC...");
	try { readTOC(); } catch (Exception *e2) { delete e2; }
	if (CDState().isBlank())
		return true;

	log.put(5, S.e + "not empty -> eject");
	toClient(hint, S.e + "not empty -> eject");
	eject();
	return false;
}

void Server::load() {
	Cdrecord p(writerNo());
	p.addArg("-load");
	p.run();
}

void Server::logConnection2() {
	File f = new File(config.getLogDir(), "connects");
	FILE *file = fopen(f.getPath().getBytes(), "a");
	if (file == NULL)
		return;

	time_t t;
	time(&t);
	String time0 = timeToString(connectionT0);
	fprintf(file, "%s %-20s ", time0.getBytes(), userID);
	char timeStr[80];
	fprintf(file, "%s %10u\n",
		secondsToString(t - connectionT0, timeStr),
		bytesReceived);
	fclose(file);
}

void Server::logSession2(const char *typeStr, String &result) {
	File f = new File(config.getLogDir(), "sessions");
	FILE *file = fopen(f.getPath().getBytes(), "a");
	if (file == NULL)
		return;

	time_t t;
	time(&t);
	String time0 = timeToString(t);
	fprintf(file, "%s %-20s ", time0.getBytes(), userID);
	char timeStr[80];
	fprintf(file, "%s ",
		secondsToString(sessionT1 - sessionT0, timeStr));
	fprintf(file, "%s ", secondsToString(t - writer().burnT0, timeStr));
	int sizeKB = CDState().getSizeKB();
	String str = "";
	str = str + (1024 * (off_t)sizeKB)
		+ " " + typeStr + " " + result.replace(' ', '_');

	if (config.getActiveBoolean("askForProjectInfo"))
		str = str
			+ " " + options.projectInfo.from.replace(' ', '_')
			+ " " + options.projectInfo.to.replace(' ', '_')
			+ " " + options.projectInfo.label.replace(' ', '_')
			+ " " + options.projectInfo.contents.replace(' ', '_');

	str = str + "\n";

	fprintf(file, "%s", str.getBytes());
	fclose(file);

	if (!result.equalsIgnoreCase("OK")
			&& config.getEMailOnError() != NULL) {
		String cmdStr = "";
		cmdStr = cmdStr
			+ "echo " + timeToString(connectionT0)
			+ " " + userID + " " + typeStr + " " + result
			+ " | mail -s CD-Writer-Error " + config.getEMailOnError();
		system(cmdStr.getBytes());
	}

	String dateTime = timeToString(t);
	String date = dateTime.substring(0, 8);
	String time = dateTime.substring(8);
	String user = new String();
	user = user + clientUserName + "@" + clientIP;
	status.addResult(date, time, user, sizeKB, result,
		options.projectInfo);

	// logCDstate
	f = new File(config.getLogDir(), "CDinfos");
	file = fopen(f.getPath().getBytes(), "a");
	if (file == NULL)
		return;
	str = "";
	char str2[4 * 1024];
	str = str + CDState().getLogStr(str2)
		+ " " + result.replace(' ', '_') + "\n";
	fprintf(file, "%s", str.getBytes());
	fclose(file);
}

void Server::nextCmd() {
	if (lineStrPending)
		lineStrPending = 0;
	else
	if (inStream.readLine(lineStr, sizeof lineStr) == NULL) {
		log.put("fgets(..., inStream) == NULL");
		quit = 1;
		return;
	}

	int l = 0;
	while (lineStr[l] != 0 && lineStr[l] != 10 && lineStr[l] != 13)
		l++;
	lineStr[l] = 0;

	l = 0;
	while (((unsigned char *)lineStr)[l] >= 32)
		l++;
	if (lineStr[l] == 0)
		try {
			executeCmd();
		}
		catch (Exception *e) {
			errorToClient(e->toString().getBytes());
			log.put(1, S.e + "exception: " + e->toString());
			delete e;
		}
		catch (const char *msg) {
			errorToClient(msg);
			log.put(1, S.e + "msg: " + msg);
		}
	else {
		openCommands++;
		errorToClient("command contains control-characters");
//		saveLineStr();
		quit = 1;
	}
}

int Server::noDisc2(int trayOpenIfNoDisc) {
	for (int i = 0; i < 20; i++) {
		Cdrecord p(writerNo());
		p.addArg("-inq");
		p.addArg("-V");
		int result = p.run();

		log.debug("Server::noDisc()",  "1 %d", result);
		if (result == 0 && p.isDriveReady())
			return 0;

		log.debug("Server::noDisc()",  "2");
		if (p.isMediumNotPresent() && p.isTrayClosed())
			if (i == 0) {
				// this seems to happen occasionally after
				// inserting a CD, so give it a second chance
				log.debug("Server::noDisc()",  "5");
				sleep(1);
				continue;
			}
			else
				if (trayOpenIfNoDisc)
					eject();

		log.debug("Server::noDisc()",  "3");
		if (!p.isInProcessOfBecomingReady())
			return 1;

		log.debug("Server::noDisc()",  "4");
		sleep(2);
	}
	return 1;
}

void Server::readATIP() {
	Cdrecord p(writerNo(), &CDState());
	String logFile = getNewLogFileName();
	p.setProtoFile(logFile.getBytes());
	p.addArg("-atip");
	p.run();
}

void Server::readMultiSessionInfo() {
	Cdrecord p(writerNo());
	String logFile = getNewLogFileName();
	p.setProtoFile(logFile.getBytes());
	p.addArg("-msinfo");

	p.start();
	int n = 0;
	const char *lineStr = p.readLine();
	if (lineStr != NULL)
		n = sscanf(lineStr, "%d,%d",
			&CDState().lastSession0, &CDState().lastSession1);

	while (1) {
		const char *lineStr = p.readLine();
		if (lineStr == NULL)
			break;
	}
	p.getResult();

	if (n == 2) {
		if (CDState().lastSession0 > CDState().lastSession1)
			throw new Exception("invalid session offset");
	}
	else {
#ifndef FreeBSD
/*		// make sure the CD is really empty
		FILE *rawCDStream = fopen(
			config.getWriterRawDevice(writer()), "r");
		if (rawCDStream != NULL) {
			char buf[512];
			int n = fread(buf, 1, sizeof buf, rawCDStream);
			fclose(rawCDStream);
			if (n == sizeof buf)
				// can't get msinfo although CD isn't empty
				throw new Exception("can't determine state of CD");
		}
*/
#endif
		CDState().lastSession0 = CDState().lastSession1 = 0;
	}

	toClient(hint, "msinfo %d %d",
		2 * CDState().lastSession0, 2 * CDState().lastSession1);
}

void Server::readTOC() {
	log.put(5, "Server::readTOC() start");
	Cdrecord p(writerNo(), &CDState());
	String logFile = getNewLogFileName();
	p.setProtoFile(logFile.getBytes());
	p.addArg("-toc");

	p.start();
	bool isBlank = true;
	while (1) {
		const char *lineStr = p.readLine();
		if (lineStr == NULL)
			break;
		String line = lineStr;
		if (line.startsWith("track:"))
			isBlank = false;
		else
		if (line.startsWith("Driver flags"))
			writer().hasBurnFree = line.indexOf("BURNFREE") > 0;
	}
	p.getResult();
//	CDState().isBlank = p.isBlankDisk();
	CDState().setIsBlank(isBlank);
	log.put(5, S.e + "Server::readTOC() isBlank = " + isBlank);
}

/**
 * convert escape sequences to characters
 */
char *Server::removeEscChars(char *str) {
	int i, j;

	if (options.protocolVersion < 4)
	// convert "\_" to " " and "\"c to c
	for (i = 0, j = 0; str[i] != 0; i++, j++) {
		if (str[i] == '\\') {
			if (str[++i] == 0)
				break;
			if (str[i] == '_')
				str[i] = ' ';
		}
		str[j] = str[i];
	}
	else
	for (i = 0, j = 0; str[i] != 0; i++, j++) {
		if (str[i] == '%') {
			if (str[++i] == 0)
				break;
			int n = 0;
			if (str[i] == 'u') {
				if (str[++i] == 0)
					break;
				n = hextoint(&str[i], 4);
				if (n >= 0)
					i += 3;
			}
			else {
				n = hextoint(&str[i], 2);
				if (n >= 0)
					i += 1;
			}

			if (n <= 0)
				str[j] = '_';
			else
			if (n < 0x0080)
				str[j] = n;
			else
			if (!config.getMkisofsWithUTF8()) {
				if (n < 0x0100)
					str[j] = n;
				else
					str[j] = '_';
			}
			else
			// use UTF-8 encoding (see RFC 2044)
			if (n < 0x0800) {
				// 110xxxxx 10xxxxxx
				str[j++] = 0xC0 | (n >> 6);
				str[j] = 0x80 | (n & 0x3F);
			}
			else {
				// 1110xxxx 10xxxxxx 10xxxxxx
				str[j++] = 0xE0 | (n >> 12);
				str[j++] = 0x80 | ((n >> 6) & 0x3F);
				str[j] = 0x80 | (n & 0x3F);
			}
		}
		else
			str[j] = str[i];
	}

	str[j] = 0;
	return str;
}

void Server::reserveDevice(int ID) {
	int cancel = waitForService(writerQueue, 0, ID);
	if (cancel)
		throw new Exception(S.e + "waiting for "
			+ (ID < 0? "reader": "writer") + " cancelled");

	int i = writerQueue->getWriter(ID);
	if (writers[i].state != idle)
		log.put(1, S.e + "FIXME: Server::reserveDevice " + ID + " " + i);

	writers[i].owner = this;
	writers[i].thread = Thread::currentThread();

	struct timeval t;
	gettimeofday(&t, NULL);
	writers[i].reservedUntil = t.tv_sec + 60 * config.getWaitForCD();
	writers[i].state = reserved;
}

void Server::resetImageFile() {
	if (config.getRemoveSessions()
		&& !config.getImageOnTheFly()) {
		FILE *out = fopen(config.getImageFile(writerNo()), "w");
		if (out != NULL)
			fclose(out);
	}
}

void Server::resetStatic() {
	if (config.getFirstErrorPriority() >= 100)
		return;

	if (writerQueue != NULL) {
		delete writerQueue;
		writerQueue = NULL;
	}
	writerQueue = new WriterQueues();

	log.put(1, S.e + "CD-writer used: \""
		+ config.getWriterInfo(0) + "\"");

	if (store != NULL) {
		delete store;
		store = NULL;
	}
	try {
		store = new Store();
	}
	catch (Exception *e) {
		log.put(1, S.e + e->getMessage());
		delete e;
	}

	if (accounts != NULL) {
		delete accounts;
		accounts = NULL;
	}
	accounts = new Accounts();
}

void Server::run() {
	start();
	while (!quit)
		nextCmd();
}

void Server::run_cdrdaoCopy() {
	time(&writer().burnT0);
	writer().fifoPercent = 100;
	writer().percent = 0;
	writer().state = writing;
	log.putv("Server::run_cdrdaoCopy");
	Cdrdao p("copy", &CDState());

	// options to cdrdao
	p.addArg("-n");
	p.addArg("--on-the-fly");
	addArgs(p, config.getCdrdaoArgs());
	if (options.laserOff || !config.getLaserOn())
		p.addArg("--simulate");
//	if (!options.closeCD)
//		p.addArg("--multi");
	p.addArg("--speed");
	if (options.speed > 0)
		p.addArg(S.e + options.speed);
	else
		p.addArg(S.e + config.getDefaultSpeed());
	p.addArg("-v");
	p.addArg("2");
	p.addArg("--source-device");
	p.addArg(config.getWriter(readerNo));
	p.addArg("--device");
	p.addArg(config.getWriter(writerNo()));

	p.show();
	String logFile = getNewLogFileName();
	p.setProtoFile(logFile.getBytes());
	p.start();

	while (1) {
		const char *lineStr = p.readLine();
		if (lineStr == NULL)
			break;

		toClient("CDRECORD", "%s", lineStr);

		char str[1024];
		switch (p.getLineType()) {
			case Cdrdao::progress:
			int a, b;
			p.getProgress(a, b);
			toClient(hint, "Track 01: %d of %d", a, b);
			if (b == 0)
				writer().percent = 100;
			else
				writer().percent = 100 * a / b;
			writer().fifoPercent = p.getFifoPercent();
			break;
		}
	}
}

void Server::run_cdrecord() {
	try {
		run_cdrecord2();
	}
	catch (Exception *e) {
		if (e->getMessage().equals("restartWithoutMulti")) {
			options.closeCD = 1;
			run_cdrecord2();
		}
		else
		if (e->getMessage().equals("perhaps a DVD+RW"))
			// for DVD+RW support
			writeImageToRawDevice();
		else
			throw e;
	}
}

void Server::run_cdrecord2() {
	time(&writer().burnT0);
	writer().fifoPercent = 100;
	writer().percent = 0;
	writer().state = writing;
	log.putv("Server::run_cdrecord2");
	Cdrecord p(writerNo(), &CDState());

	// options to cdrecord
	addArgs(p, config.getCdrecordArgs());
	if (options.overburn) {
		addArgs(p, config.getActiveValue(
			"overburnSupportArgs").getBytes());
		p.setOverburn();
	}
	if (config.getBurnFree() && writer().hasBurnFree)
		p.addArg("driveropts=burnfree");
	if (options.laserOff || !config.getLaserOn())
		p.addArg("-dummy");
	if (!config.isDefault("gracetime"))
		p.addArg("gracetime=%d", config.getActiveInt("gracetime"));
	if (!options.closeCD)
		p.addArg("-multi");
	if (options.speed > 0)
		p.addArg(S.e + "speed=" + options.speed);
	else
		p.addArg(S.e + "speed=" + config.getDefaultSpeed());
	p.addArg("-v");

	// track options/tracks to cdrecord
	int makeImageOnTheFly = 0;
	File *track = session->getFirstTrack();
	if (track->isDirectory()) {
		if (config.getImageOnTheFly()) {
			makeImageOnTheFly = 1;
			p.addArg("tsize=%ds", CDState().imageSizeInBlocks);
			p.addArg("-waiti");

			// avoid data tracks < 1 MB
			int padSectors = 512 - CDState().imageSizeInBlocks;
			if (padSectors > 0)
				p.addArg("padsize=%ds", padSectors);
		}
//		p.addArg("-tao");
		p.addArg("-data");
		if (makeImageOnTheFly)
			p.addArg("-");
		else
			p.addArg(config.getImageFile(writerNo()));
	}
	else {
		if (strstr(track->getPath().getBytes(), ".wav") == NULL) {
			p.addArg("-data");
			p.addArg("-pad");
		}
		else {
			p.addArg("-pad");
			if (options.DAO)
				p.addArg("-dao");
			if (options.CDText) {
				p.addArg("-text");
				p.addArg("-useinfo");
			}
		}
		while (track != NULL) {
			p.addArg(track->getPath().getBytes());
			track = session->getNextTrack();
		}
	}

	// start mkisofs for imageOnTheFly
	Process mkisofs(config.getMkisofsPath());
	if (makeImageOnTheFly) {
		start_mkisofs(mkisofs);

		// try to set the scheduler of mkisofs to RR 97
		// (cdrecord seems to use RR 98 and 99)
		struct sched_param schedParam;
		schedParam.sched_priority = 97;
		if (mkisofs.getPID() > 0)
			if (sched_setscheduler(mkisofs.getPID(),
					SCHED_RR, &schedParam) != 0) {
				// try external variant
				Process p2(config.getSetSchedulerPath());
				p2.addArg(S.e + mkisofs.getPID());
				p2.addArg("RR");
				p2.addArg(S.e + schedParam.sched_priority);
				p2.run();
				if (p2.getResult() != 0)
					log.put(2, S.e + "Cannot set mkisofs scheduler to RR "
						+ schedParam.sched_priority);
			}

		p.setInputStream(dup(mkisofs.getInputStream()));
	}

	p.show();
	String logFile = getNewLogFileName();
	p.setProtoFile(logFile.getBytes());
	p.start();
	int quit = 0;
	while (!quit) {
		fd_set fdSet;
		int seconds = 0;
		while (true) {
			FD_ZERO(&fdSet);
			if (makeImageOnTheFly
					&& (writer().state != fixating
						|| writer().percent >= 100))
				FD_SET(mkisofs.getErrorStream(), &fdSet);
			FD_SET(p.getInputStream(), &fdSet);
			struct timeval tv;
			if (writer().state == fixating
					&& writer().percent < 100)
				tv.tv_sec = 1;
			else
				tv.tv_sec = 60 * 60;
			tv.tv_usec = 0;
			if (select(FD_SETSIZE, &fdSet, NULL, NULL, &tv) == 0) {
				// timeout
				if (writer().state != fixating
						|| seconds >= 10 * 60)
					throw new Exception("cdrecord timeout");
				seconds++;
				int secondsEstimated = p.getFixationSeconds();
				if (secondsEstimated <= 0)
					secondsEstimated = 4 * 60;
				int percent = 100 * seconds / secondsEstimated;
				if (percent > 100)
					percent = 100;
				writer().percent = percent;
				toClient(hint, "%s %d", hintPercentDone, percent);
				if (percent < 100)
					log.debug("fixating", S.e + "percent = " +  percent);
				else
					log.put(5, S.e + "Fixating percent = " +  percent);
			}
			else
				break;
		}
		if (makeImageOnTheFly
			&& FD_ISSET(mkisofs.getErrorStream(), &fdSet))
			// read all available lines from mkisofs
			for (int i = 1; ; i++) {
				log.debug("imageOTF", "mkisofs %d", i);
				const char *lineStr = mkisofs.readLine();
//				if (lineStr != NULL)
//					toClient("MKISOFS", "%s", lineStr);
				if (!mkisofs.isLineAvailable())
					break;
			}

		if (!FD_ISSET(p.getInputStream(), &fdSet))
			continue;

		// read all available lines from cdrecord
		for (int i = 1; ; i++) {
			log.debug("imageOTF", "cdrecord %d", i);
			const char *lineStr;
			try {
				lineStr = p.readLine();
			}
			catch (Exception *e) {
				throw e;
			}
			if (lineStr == NULL) {
				quit = 1;
				break;
			}
			toClient("CDRECORD", "%s", lineStr);

			char str[1024];
			switch (p.getLineType()) {
				case Cdrecord::capacity:
				toClient(hint, "%s",
					CDState().getCapacityStr(str));
				break;

				case Cdrecord::progress:
				int a, b;
				p.getProgress(a, b);
				toClient(hint, "Track 01: %d of %d", a, b);
				if (b == 0)
					writer().percent = 100;
				else
					writer().percent = 100 * a / b;
				writer().fifoPercent = p.getFifoPercent();
				break;

				default:
				if (strstr(lineStr, "Fixating...") == lineStr) {
					writer().fifoPercent = 100;
					writer().percent = 0;
					writer().state = fixating;
					toClient(hint, hintFixating);
				}
				else
				if (strstr(lineStr, "Fixating time:") == lineStr) {
					writer().percent = 100;
					toClient(hint, "%s %d", hintPercentDone, 100);
					log.put(5, S.e + "Fixating speed = " + CDState().speed
						+ (CDState().isRW? ", RW": "")
						+ (CDState().blocks1 == CDState().blocks0?
							", first session": "")
						+ ", estimated = " + p.getFixationSeconds());
				}
			}

			if (!p.isLineAvailable())
				break;
		}
	}

	if (p.getResult() == 0) {
		if (CDState().isClosed)
			toClient(hint, hintCDwasClosed);
	}
	else
		if (p.getFirstError() == NULL)
			throw new Exception("unknown cdrecord error");
		else
			throw new Exception(p.getFirstError());
}

void Server::run_cdrecordBlank(const char *modeStr) {
	writer().fifoPercent = 100;
	writer().percent = 0;
	writer().state = blanking;
	log.putv("Server::run_cdrecordBlank");
	Cdrecord p(writerNo(), &CDState());

	p.addArg("blank=%s", modeStr);
//	p.addArg("-force");
	if (!config.isDefault("gracetime"))
		p.addArg("gracetime=%d", config.getActiveInt("gracetime"));
	if (options.speed > 0)
		p.addArg(S.e + "speed=" + options.speed);
	else
		p.addArg(S.e + "speed=" + config.getDefaultSpeed());
	p.addArg("-v");

	p.show();
	String logFile = getNewLogFileName();
	p.setProtoFile(logFile.getBytes());
	p.start();

	time_t t0;
	time(&t0);
	while (1) {
		try {
			const char *lineStr = p.readLine(1);
			if (lineStr == NULL)
				break;
			toClient("CDRECORD", "%s", lineStr);
		} catch (Exception *exception) {
			if (!exception->getMessage().equals("timeout"))
				throw exception;

			int estimate = 80 * 60;
			int speed = CDState().speed;
			if (speed <= 0)
				speed = 1;
			if (strstr(modeStr, "fast") != NULL)
				estimate = 9 + 240 / speed;
			else
				if (CDState().blocks0 > 0)
					estimate = 9 + CDState().blocks0 / 75 / speed;
			estimate = (12 * estimate) / 10;
			time_t t;
			time(&t);
			float percent = 100.0 * (t - t0) / estimate;
			if (percent >= 100)
				percent = 99;
			toClient(hint, "%s %f", hintPercentDone, percent);
			writer().percent = (int)percent;
			delete exception;
		}
	}

	if (p.getResult() != 0)
		if (p.getFirstError() == NULL)
			throw new Exception("unknown cdrecord error");
		else
			throw new Exception(p.getFirstError());

	toClient(hint, "%s %f", hintPercentDone, 100);
	writer().percent = 100;
	CDState().setIsBlank(true);
}

void Server::run_cdrecordCopy() {
	time(&writer().burnT0);
	writer().fifoPercent = 100;
	writer().percent = 0;
	writer().state = writing;
	log.putv("Server::run_cdrecordCopy");
	Cdrecord p(writerNo(), &CDState());

	// options to cdrecord
	addArgs(p, config.getCdrecordArgs());
	if (config.getBurnFree() && writer().hasBurnFree)
		p.addArg("driveropts=burnfree");
	if (options.laserOff || !config.getLaserOn())
		p.addArg("-dummy");
	if (!config.isDefault("gracetime"))
		p.addArg("gracetime=%d", config.getActiveInt("gracetime"));
	if (!options.closeCD)
		p.addArg("-multi");
	if (options.speed > 0)
		p.addArg(S.e + "speed=" + options.speed);
	else
		p.addArg(S.e + "speed=" + config.getDefaultSpeed());
	p.addArg("-v");
	p.addArg("-isosize");
	p.addArg(config.getWriterRawDevice(readerNo));

	p.show();
	String logFile = getNewLogFileName();
	p.setProtoFile(logFile.getBytes());
	p.start();

	while (1) {
		const char *lineStr = p.readLine();
		if (lineStr == NULL)
			break;
		toClient("CDRECORD", "%s", lineStr);

		char str[1024];
		switch (p.getLineType()) {
			case Cdrecord::capacity:
			toClient(hint, "%s",
				CDState().getCapacityStr(str));
			break;

			case Cdrecord::progress:
			int a, b;
			p.getProgress(a, b);
			toClient(hint, "Track 01: %d of %d", a, b);
			if (b == 0)
				writer().percent = 100;
			else
				writer().percent = 100 * a / b;
			writer().fifoPercent = p.getFifoPercent();
			break;

			default:
			if (strstr(lineStr, "Fixating...") == lineStr) {
				writer().fifoPercent = 100;
				writer().percent = 0;
				writer().state = fixating;
				toClient(hint, hintFixating);
			}
		}
	}

	if (p.getResult() == 0) {
		if (CDState().isClosed)
			toClient(hint, hintCDwasClosed);
	}
	else
		if (p.getFirstError() == NULL)
			throw new Exception("unknown cdrecord error");
		else
			throw new Exception(p.getFirstError());
}

void Server::run_mkisofs() {
	writer().percent = 0;
	writer().state = mkisofs;
	log.put(2, S.e + "Server::run_mkisofs "
		+ session->getFirstTrack()->getPath());

	Process p(config.getMkisofsPath());
	if (config.getImageOnTheFly())
		p.addArg("-print-size");
	start_mkisofs(p);
	while (1) {
		const char *lineStr;
		try {
			lineStr = p.readLine(2 * 60);
		} catch (Exception *e) {
			delete e;
			p.terminate();
			throw new Exception("mkisofs timeout");
		}
		if (lineStr == NULL)
			break;
		toClient("MKISOFS", "%s", lineStr);

		sscanf(lineStr,
			"Total extents scheduled to be written = %d",
			&CDState().imageSizeInBlocks);

		char *str;
		long int n = strtol(lineStr, &str, 10);
		if (str == lineStr)
			continue;

		if (strstr(str, " extents written") == str) {
			CDState().extentsWrittenKB = 2 * n;
			log.put("mkisofs extentsWritten = %d MB",
				CDState().extentsWrittenKB / 1024);
		}
		if (*str == ' ')
			toClient(hint, "KByte %ld", 2 * n);
		if (*str == '.') {
			float percent;
			if (sscanf(lineStr, "%f", &percent) == 1) {
				toClient(hint, "%s %f", hintPercentDone, percent);
				writer().percent = (int)percent;
			}
		}
	}

	if (p.getResult() != 0)
		throw new Exception("can't make CD-image");
}

void Server::run_readcd() {
	if (config.getActiveBoolean(SIMULATION))
		return;

	log.put(2, S.e + "Server::run_readcd");

	Process p(config.getReadcdPath());
	p.addArg(S.e + "dev=" + config.getWriter(writerNo()));
	p.addArg(S.e + "speed=" + config.getMaxSpeed(writerNo(), "CD"));
	p.addArg(S.e + "sectors=0-1");
	p.addArg(S.e + "f=/tmp/CD-" + writerNo() + "-S0");

	p.show();
	String logFile = getNewLogFileName();
	p.setProtoFile(logFile.getBytes());
	p.start();

	while (true) {
		const char *lineStr;
		try {
			lineStr = p.readLine(2 * 60);
		} catch (Exception *e) {
			delete e;
			p.terminate();
			throw new Exception("readcd timeout");
		}
		if (lineStr == NULL)
			break;
	}

	if (p.getResult() != 0)
		throw new Exception("readcd failed");
}

void Server::saveLineStr() {
	static int no = 0;
	char str[1024];
	sprintf(str, "%s/lineStr/pid%04d.%03d",
		config.getLogDir(), getpid(), no++);
	makeDirs(str);
	FILE *outStream = fopen(str, "w");
	if (outStream == NULL) {
		log.put("saveLineStr(): cannot save lineStr");
		return;
	}
	int n = fwrite(lineStr, 1, sizeof lineStr, outStream);
	if (n != sizeof lineStr)
		log.put("saveLineStr(): n != sizeof lineStr");
	fclose(outStream);
}

void Server::showFeatures() {
	if (config.getAnonymousUse())
		toClient(hint, "anonymousUse");

	if (config.getActiveBoolean("askForProjectInfo"))
		toClient(hint, "askForProjectInfo");

	if (config.getActiveBoolean("audioCDSupport"))
		toClient(hint, "audioCDSupport");

	if (config.getCdrdao())
		toClient(hint, "cdrdao");

	if (config.getCopySupport())
		toClient(hint, "copySupport");

	toClient(hint, "defaultSpeed %d %d %d", config.getDefaultSpeed(),
		config.getActiveInt(DEFAULTSPEEDAUDIO),
		config.getActiveInt(DEFAULTSPEEDCDRW));

	int md5 = config.getActiveInt("feature.calculateMD5");
	if (md5 != 2)
		toClient(hint, S.e + "md5=" + md5);

	if (config.getActiveBoolean("PAM"))
		toClient(hint, "managedAccounts");

	if (config.getMaxCopies() > 1)
		toClient(hint, S.e + "maxCopies " + config.getMaxCopies());

	toClient(hint, S.e + "maxSpeed " + config.getMaxSpeed());
	toClient(hint, S.e + "maxMBTransferPerProject "
		+ config.getMaxMBytesPerSession());

	if (config.getActiveBoolean("MP3decoding"))
		toClient(hint, "mp3");

	if (config.getActiveBoolean("oggDecoding"))
		toClient(hint, "ogg");

	if (config.getActiveBoolean("overburnSupport"))
		toClient(hint, "overburnSupport");

	if (config.getVerify() > 0)
		toClient(hint, S.e + "verify=" + config.getVerify());
}

void replaceFirst(StringBuffer &buf, const char *key, String &by) {
	int pos = buf.indexOf(key);
	if (pos < 0)
		return;
	buf.delete_(pos, pos + strlen(key));
	buf.insert(pos, by.getBytes());
}

void Server::showWaitForDiscMsg() {
	FILE *inStream = fopen(config.getWaitForCDMsgFile(), "r");
	if (inStream == NULL)
		toClient(info, S.e + "Please insert a " + options.medium + "!");
	else {
		while (1) {
			char line[1024];
			fgets(line, sizeof line, inStream);
			if (feof(inStream))
				break;
			StringBuffer workBuf = new StringBuffer(line);
			String str = "";
			replaceFirst(workBuf, "\n", str);
			str = str + config.getWaitForCD();
			replaceFirst(workBuf, "%d", str);
			str = "";
			str = str + config.getWaitForCD();
			replaceFirst(workBuf, "$reservedMinutes", str);
			str = "";
			str = str + (writerNo() + 1);
			replaceFirst(workBuf, "$writerNo", str);
			replaceFirst(workBuf, "$writerType",
				config.getWriterInfo(writerNo()));
			if (options.medium.startsWith("DVD"))
				replaceFirst(workBuf, "CD-R(W)", options.medium);
			str = new String(workBuf);
			toClient(info, S.e + str);
		}
		fclose(inStream);
	}
}

char toHexDigit(int n) {
	if (n < 0)
		return 0;
	else
	if (n < 10)
		return '0' + n;
	else
	if (n < 16)
		return 'A' + n - 10;
	else
		return 'F';
}

const char *Server::spaceToEsc(const char *str) {
	char str2[6 * strlen(str) + 1];

	int i, j;
	for (i = 0, j = 0; str[i] != 0; ) {
		// UTF-8 decoder
		int code;
		if ((str[i] & 0x80) == 0x00)
			code = str[i++];
		else
		if ((str[i] & 0xE0) == 0xC0
				&& (str[i] & 0x1E) != 0x00
				&& (str[i + 1] & 0xC0) == 0x80) {
			code = str[i++] & 0x1F;
			code = (code << 6) | (str[i++] & 0x3F);
		}
		else
		if ((str[i] & 0xF0) == 0xE0
				&& (str[i] & 0x0F) != 0x00
				&& (str[i + 1] & 0xC0) == 0x80
				&& (str[i + 1] & 0x20) != 0x00
				&& (str[i + 2] & 0xC0) == 0x80) {
			code = str[i++] & 0x0F;
			code = (code << 6) | (str[i++] & 0x3F);
			code = (code << 6) | (str[i++] & 0x3F);
		}
		else
			code = str[i++];

		if (code > 0x20 && code < 0x7F && code != '%')
			str2[j++] = code;
		else
		if (code < 0x100) {
			str2[j++] = '%';
			str2[j++] = toHexDigit((code & 0x00F0) >> 4);
			str2[j++] = toHexDigit(code & 0x000F);
		}
		else {
			str2[j++] = '%';
			str2[j++] = 'u';
			str2[j++] = toHexDigit((code & 0xF000) >> 12);
			str2[j++] = toHexDigit((code & 0x0F00) >> 8);
			str2[j++] = toHexDigit((code & 0x00F0) >> 4);
			str2[j++] = toHexDigit(code & 0x000F);
		}
	}

	str2[j] = 0;
	return strDup(str2);
}

void Server::start() {
	String licenseStr = "";
	String license0 = getLicense();
	if (license0.length() > 0)
		licenseStr = licenseStr + " (" + license0 + ")";
	toClient(info, S.e + Version.product + Version.productPro
		+ " " + Version.version + licenseStr);
	showFeatures();

	FILE *inStream = fopen(config.getInfoFile(), "r");
	if (inStream != NULL) {
		while (1) {
			char str[1024];
			fgets(str, sizeof str, inStream);
			if (feof(inStream))
				break;
			toClient(info, str);
		}
		fclose(inStream);
	}

	openCommands++;
	if (config.getFirstErrorPriority() >= 100) {
		char str[4 * 1024];
		gethostname(str, sizeof str);
		errorToClient("Server Configuration Error"
			" - see http://%s:%d/ for details",
			str, config.getPortNo());
		quit = 1;
	}
	else
	if (activeServersNum > min(config.getMaxOpenConnections(), maxUsers)) {
		errorToClient(S.e
			+ min(config.getMaxOpenConnections(), maxUsers)
			+ " users (the maximum) are already connected");
		quit = 1;
	}
	else
		successToClient("connected");
}

void Server::start_mkisofs(Process &p) {
	if (hasWriter()
			&& !CDState().isLastSessionInfoValid()
			&& (CDState().isBlankUnknown() || !CDState().isBlank())
			&& CDState().mediaType.startsWith("CD-R"))
		readMultiSessionInfo();

	String logFile = getNewLogFileName();
	p.setProtoFile(logFile.getBytes());

	// arguments for mkisofs
	addArgs(p, config.getMkisofsArgs());
	if (options.bootImage.length() > 0) {
		p.addArg("-eltorito-boot");
		p.addArg(options.bootImage.getBytes());
		File file = new File(
			*(session->getFirstTrack()), options.bootImage);
		if (file.length() != 1200 * 1024
			&& file.length() != 1440 * 1024
			&& file.length() != 2880 * 1024)
			if (file.length() < 10 * 1024 * 1024)
				p.addArg("-no-emul-boot");
			else
				p.addArg("-hard-disk-boot");
	}
	if (options.HFS)
		p.addArg("-hfs");
	if (options.joliet)
		p.addArg("-joliet");
//	if (session->getLinksNum() > 0)
		p.addArg("-follow-links");
	p.addArg("-full-iso9660-filenames");
//	p.addArg("-allow-leading-dots");
	if (options.rockRidge)
		p.addArg("-rational-rock");
//	p.addArg("-input-charset";
//	p.addArg("ISO-8859-1");
	p.addArg("-verbose");
	p.addArg("-gui");
	if (hasWriter()
			&& !config.getImageOnTheFly()
			&& CDState().mediaType.startsWith("CD-R")) {
		p.addArg("-o");
		p.addArg(config.getImageFile(writerNo()));
	}
	if (options.volumeID.length() == 0)
		options.volumeID = timeToShortString();
	if (options.HFS)
		while (true) {
			int p = options.volumeID.indexOf(":");
			if (p < 0)
				break;
			String str1 = options.volumeID.substring(0, p);
			str1 = str1 + "_" + options.volumeID.substring(p + 1);
			options.volumeID = str1;
		}
	p.addArg("-volid");
	if (options.volumeID.length() > 32)
		options.volumeID = options.volumeID.substring(0, 32);
	p.addArg(options.volumeID.getBytes());

	p.addArg("-preparer");
	String licenseStr = "";
	String license0 = getLicense();
	if (license0.length() > 0)
		licenseStr = licenseStr + " (" + license0 + ")";
	String preparerID;
	preparerID = preparerID
		+ Version.product + Version.productPro
		+ " " + Version.version + licenseStr;
	preparerID = addKey(preparerID);
	if (preparerID.length() > 128)
		preparerID = preparerID.substring(0, 128);
	p.addArg(preparerID.getBytes());

	p.addArg("-publisher");
	String publisherID;
	publisherID = publisherID
		+ timeToString(sessionT1)
		+ ", User: " + userID
		+ ", Org: " + config.getOrganization();
	if (publisherID.length() > 128)
		publisherID = publisherID.substring(0, 128);
	p.addArg(publisherID.getBytes());

	if (hasWriter()
			&& CDState().lastSession1 > 0
			&& CDState().mediaType.startsWith("CD-R")) {
		p.addArg("-C");
		p.addArg(S.e + CDState().lastSession0 + "," + CDState().lastSession1);
		if (!options.blankTOC && !options.hideContents) {
			p.addArg("-M");
			p.addArg(config.getWriter(writerNo()));
		}
	}

	// add a directory that contains files that should be on each CD
	File defaultFilesDir(config.getDefaultFilesDir());
	if (defaultFilesDir.exists())
		p.addArg(defaultFilesDir.getPath().getBytes());

	p.addArg(".");
	p.show();

	p.start(session->getFirstTrack()->getPath().getBytes(),
		// splitOut
		!hasWriter()
			|| (config.getImageOnTheFly()
				&& CDState().mediaType.startsWith("CD-R")));
}

void Server::successToClient(const char *format, ...) {
	if (isOffline)
		return;
	if (openCommands == 0) {
		log.put("error: openCommands = %d", openCommands);
		return;
	}

	char str[8 * 1024];
	va_list ap;
	va_start(ap, format);
	vsnprintf(str, sizeof str - 1, format, ap);
	va_end(ap);

	// Nachricht an Client besteht aus genau einer Zeile
	int i = 0;
	while (str[i] != 0 && str[i] != '\n')
		i++;
	str[i++] = '\n';
	str[i] = 0;

	char lineStr[16 * 1024];
	sprintf(lineStr, "OK %s", str);
	int n = socket.write(lineStr, strlen(lineStr));
	openCommands--;
}

void Server::terminate() {
}

String *Server::timeToDigits(time_t t) {
	if (t == 0)
		time(&t);
	struct tm *tm = localtime(&t);
	String *str = new String();
	*str = *str
		+ (1900 + tm->tm_year)
		+ (tm->tm_mon + 1 < 10? "0": "") + (tm->tm_mon + 1)
		+ (tm->tm_mday < 10? "0": "") + tm->tm_mday
		+ (tm->tm_hour < 10? "0": "") + tm->tm_hour
		+ (tm->tm_min < 10? "0": "") + tm->tm_min
		+ (tm->tm_sec < 10? "0": "") + tm->tm_sec;
	return str;
}

String *Server::timeToShortString() {
	time_t t;
	time(&t);
	struct tm *tm = localtime(&t);
	String *str = new String();
	*str = *str
		+ (1900 + tm->tm_year)
		+ "-" + (tm->tm_mon + 1 < 10? "0": "") + (tm->tm_mon + 1)
		+ "-" + (tm->tm_mday < 10? "0": "") + tm->tm_mday
		+ " " + (tm->tm_hour < 10? "0": "") + tm->tm_hour
		+ ":" + (tm->tm_min < 10? "0": "") + tm->tm_min;
	return str;
}

String *Server::timeToString(time_t t) {
	if (t == 0)
		time(&t);
	struct tm *tm = localtime(&t);
	String *str = new String();
	*str = *str
		+ (1900 + tm->tm_year)
		+ (tm->tm_mon + 1 < 10? "0": "") + (tm->tm_mon + 1)
		+ (tm->tm_mday < 10? "0": "") + tm->tm_mday
		+ " " + (tm->tm_hour < 10? "0": "") + tm->tm_hour
		+ ":" + (tm->tm_min < 10? "0": "") + tm->tm_min
		+ ":" + (tm->tm_sec < 10? "0": "") + tm->tm_sec;
	return str;
}

void Server::toClient(int code, String &text, int logLevel) {
	if (!isOffline) {
		int p0 = text.indexOf("\n");
		if (p0 >= 0)
			text = text.substring(0, p0);

		String lineStr = "";
		lineStr = lineStr + code + " " + text + "\n";
		int n = socket.write(lineStr.getBytes(), lineStr.length());

		if (logLevel < 10)
			log.put(logLevel, S.e + code + " " + text);
	}

	String::free(&text);
}

void Server::toClient(const char *type, const char *format, ...) {
	if (isOffline
		|| (!modeShowCdrecord && strcmp(type, "CDRECORD") == 0)
		|| (!modeShowMkisofs && strcmp(type, "MKISOFS") == 0))
		return;

	char str[8 * 1024];
	va_list ap;
	va_start(ap, format);
	vsnprintf(str, sizeof str - 1, format, ap);
	va_end(ap);

	// message to client is exactly one line
	int i = 0;
	while (str[i] != 0 && str[i] != '\n')
		i++;
	str[i++] = '\n';
	str[i] = 0;

	char lineStr[16 * 1024];
	sprintf(lineStr, "%s %s", type, str);
	int n = socket.write(lineStr, strlen(lineStr));
}

void Server::toClient(const char *type, String &str) {
	if (!isOffline
			&& (modeShowCdrecord || strcmp(type, "CDRECORD") != 0)
			&& (modeShowMkisofs || strcmp(type, "MKISOFS") != 0)) {
		int p0 = str.indexOf("\n");
		if (p0 >= 0)
			str = str.substring(0, p0);
		String lineStr = type;
		lineStr = lineStr + " " + str + "\n";
		int n = socket.write(lineStr.getBytes(), lineStr.length());
	}

	String::free(&str);
}

void Server::verify() {
	log.put(3, "Server::verify()");
	if (options.laserOff || !config.getLaserOn())
		return;

	if (session != NULL
		&& !session->getFirstTrack()->isDirectory()
		&& strstr(session->getFirstTrack()->getPath().getBytes(),
			".iso") == NULL)
		// cannot yerify audio CDs
		return;

	writer().percent = 0;
	writer().state = verifying;
	toClient(hint, hintVerifying);

	if (ejectBeforeVerify) {
		eject();
		load();
	}
	else
		ejectBeforeVerify = true;

	try {
		run_readcd();
	}
	catch (Exception *e) {
		log.put(2, S.e + "run_readcd(): " + e->getMessage());
		delete e;
	}

	Process p(config.getVerifyPath());
	int maxVerifySpeed = config.getMaxSpeed(writerNo(), "CD");
	if (maxVerifySpeed > 0)
		p.addArg(S.e + "maxSpeed=" + maxVerifySpeed);
	if (session != NULL)
		p.addArg(session->getFirstTrack()->getPath().getBytes());
	else
		p.addArg(config.getWriterRawDevice(readerNo));

	p.addArg(config.getWriterRawDevice(writerNo()));
	p.show();
	String logFile = getNewLogFileName();
	p.setProtoFile(logFile.getBytes());
	p.start();

	int numOfErrors = 0;
	String lastMessage = "";
	bool cancelled = false;
	while (true) {
		const char *lineStr = p.readLine();
		if (lineStr == NULL)
			break;
		if (strlen(lineStr) > 0)
			toClient("CDWverify", "%s", lineStr);

		int MBytes, files;
		if (sscanf(lineStr, "%d MB, %d", &MBytes, &files) == 2) {
			int sizeKB = CDState().getSizeKB();
			if (sizeKB <= 0)
				sizeKB = 100 * 1024;
			int percent = (100 * 1024 * MBytes) / sizeKB;
			toClient(hint, "%s %d", hintPercentDone, percent);
			writer().percent = percent;
		}
		else
		if (strstr(lineStr, "error:") == lineStr) {
			if (config.getLogLevel() < 5)
				log.put(2, lineStr);
			lastMessage = lineStr;
			numOfErrors++;
		}
		else
			lastMessage = lineStr;

		if (!cancelled && isCommandCancelled()) {
			log.put(5, "cancelling CDWverify...");
//			p.terminate();
//			log.put(5, "cancelling CDWverify...done");
			p.sendTerminateSignal();
			cancelled = true;
//			break;
		}
	}

	if (numOfErrors > 0
		&& session != NULL
		&& session->getFirstTrack()->isDirectory())
		throw new Exception(S.e + numOfErrors + " verify errors");

	if (!cancelled && p.getResult() != 0)
		throw new Exception(S.e + lastMessage);
}

int Server::waitForDisc() {
	if (config.getEjectOnReservation())
		eject();
	CDState().reset();

	showWaitForDiscMsg();

	if (options.burnOffline)
		successToClient("burning offline");
	else
		toClient(hint, hintWriterReserved);
	isOffline = options.burnOffline;

	log.put(2, "writer reserved - waiting for CD");

	// wait for CD, new command on socket or timeout
	int cancel = 0, timeout = 0;
	while ((noDisc(1) || !isMediumOK()) && !cancel && !timeout) {
		if (options.burnOffline)
			sleep(5);
		else
			cancel = filterCancel(inStream.waitForInput(5) != 0);

		struct timeval t;
		gettimeofday(&t, NULL);
		timeout = t.tv_sec > writer().reservedUntil;
	}

	if (cancel || timeout)
		load();

	if (cancel)
		throw new Exception("reservation cancelled");
	else
	if (timeout)
		throw new Exception("reservation timeout");
	else {
		toClient(hint, hintCDdetected);
		return 0;
	}
}

int Server::waitForService(Queue *queue, int toRank) {
	return waitForService(queue, toRank, serverNo);
}

int Server::waitForService(Queue *queue, int toRank, int ID) {
	log.put(2, S.e + "Server::waitForService " + toRank);

	// wait for rank <= toRank or cancel by new command on socket
	int rank, oldRank;
	rank = queue->getRank(ID);
	oldRank = rank + 1;
	int cancel = 0;
	while (rank > toRank && !cancel) {
		if (rank < oldRank) {
			log.put(3, S.e + "Server::waitForService"
				+ " until " + (toRank + 1)
				+ " now " + (rank + 1));
			toClient(hint, S.e + "rank " + (rank - toRank));
		}
		if (isOffline)
			sleep(1);
		else {
			statusToClient();
			cancel = filterCancel(inStream.waitForInput(1) != 0);
		}
		oldRank = rank;
		rank = queue->getRank(ID);
	}

	if (cancel)
		log.put(2, "Server::waitForService cancelled");
	else
		log.put(2, S.e + "Server::waitForService "
			+ rank + " <= " + toRank);

	return cancel;
}

void Server::writeImageToRawDevice() {
	throw new Exception("Invalid medium!");
}
