/*
 * @(#)Server.cpp
 *
 * This file is part of webCDwriter - Network CD Writing.
 *
 * Copyright (C) 1999-2002 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 "Support.h"
#include "Process.h"
#include "Version.h"
#include "Log.h"
#include "MP3.h"
#include "Config.h"
#include "Queue.h"
#include "Session.h"
#include "Server.h"
#include "Store.h"
#include "Status.h"
#include "Wave.h"

const char
	errorInCommand[] = "error in command",
	hint[] = "HINT",
	info[] = "INFO",
	*unknown = "unknown";

/**
 * Create a new server process.
 *
 * @version 20021026
 * @author Jörg P. M. Haeger
 */
Server::Server(int socket) {
	time(&connectionT0);
	char timeStr[80];
	log.put(2, "Server::Server started %s",
		timeToString(connectionT0, timeStr, sizeof timeStr));

	activeServersNum++;
	serverNo = nextServerNo++;
	sock = socket;
	inStream = fdopen(sock, "r");
	logFilesNum = 0;

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

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

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

	// modes
	modeShowCdrecord = 0;
	modeShowMkisofs = 0;

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

	toClient(info, "%s %s\nusing \"%s\"",
		product, version, config.getWriterInfo(0));

	if (config.getAnonymousUse())
		toClient(hint, "anonymousUse");

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

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

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

	if (config.getDVDplusRWSupport(0))
		toClient(hint, "DVDplusRWSupport");

	if (config.getMaxCopies() > 1)
		toClient(hint, "maxCopies %d", config.getMaxCopies());

	toClient(hint, "maxSpeed %d", config.getMaxSpeed());

	if (config.getMP3decoding())
		toClient(hint, "mp3");

	if (config.getVerify())
		toClient(hint, "verify");

	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 (configError)
	{
		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 > config.getMaxOpenConnections())
	{
		errorToClient("%d users (the maximum) are already connected",
			activeServersNum);
		quit = 1;
	}
	else
		successToClient("connected");
}

int Server::activeServersNum = 0;
int Server::nextServerNo = 1;
Queue *Server::readerQueue = NULL;
Writer *Server::readers = new Writer[maxNumOfReaders];
Queue *Server::writerQueue = NULL;
Writer *Server::writers = new Writer[maxNumOfWriters];
int Server::configError = 0;

Server::~Server() {
	close(sock);

	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;
	}

	if (clientIP != unknown)
		delete[] clientIP;

	activeServersNum--;

	time_t t;
	time(&t);
	char timeStr[80];
	log.put(2, "Server::~Server terminated %s",
		timeToString(t, timeStr, sizeof timeStr));
}

#include "DVDplusRW.h"

void Server::addArgs(Process &p, const char *args)
{
	char str[strlen(args) + 1];
	for (int i = 0; ; i++, args++)
	{
		str[i] = *args;
		if (str[i] == 0 || str[i] == ' ' || str[i] == '\t')
		{
			str[i] = 0;
			if (i > 0)
				p.addArg(str);
			i = -1;
		}
		if (*args == 0)
			break;
	}
}

void Server::blank(char *cmdStr)
{
	int hasWriter = 0;
	try {
		writerQueue->add(serverNo);
		reserveWriter();
		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);

		writers[writerNo].percent = 100;
		try { readTOC(); } catch (const char *msg) {}
		if (config.getEjectOnDone())
			eject();
		log.put("cdrecord success");
		successToClient("cdrecord success");

		writers[writerNo].state = idle;
		writerQueue->remove(serverNo);
	}
	catch (const char *msg) {
		if (hasWriter)
		{
			if (!noDisc() && config.getEjectOnDone())
				try { eject(); } catch (const char *msg) {}
			writers[writerNo].state = idle;
		}
		writerQueue->remove(serverNo);
		throw msg;
	}
}

void Server::burnCopy()
{
	int hasReader = 0, hasWriter = 0;
	try {
		readerQueue->add(serverNo);
		reserveReader();
		hasReader = 1;
		ejectReader();

		writerQueue->add(serverNo);
		reserveWriter();
		hasWriter = 1;
		time(&burnT0);

		toClient(info, "Please insert the master CD "
			"into the CD-ROM drive No. %d!", readerNo + 1);

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

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

			toClient(hint, hintCDrecordStarted);
			readers[readerNo].state = reading;
			if (config.getCdrdao()) {
				run_cdrdaoCopy();
				if (options.verify && CDstate.firstTrackIsData)
					verify();
			}
			else {
				run_cdrecordCopy();
				if (options.verify)
					verify();
			}
		}
		catch (const char *msg) {
			try { readTOC(); } catch (const char *msg) {}
			logSession("C", msg);
			throw msg;
		}

		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");

		writers[writerNo].state = idle;
		writerQueue->remove(serverNo);

		readers[readerNo].state = idle;
		readerQueue->remove(serverNo);
	}
	catch (const char *msg) {
		if (hasWriter)
		{
			if (!noDisc())
				try { eject(); } catch (const char *msg) {}
			writers[writerNo].state = idle;
		}
		if (hasReader)
		{
			ejectReader();
			writerQueue->remove(serverNo);
			readers[readerNo].state = idle;
		}
		readerQueue->remove(serverNo);
		throw msg;
	}
}

void Server::burnSession(char *cmdStr)
{
	// end of transfer
	time(&sessionT1);
	int hasWriter = 0;
	try {
		writerQueue->add(serverNo);
		reserveWriter();
		hasWriter = 1;
		time(&burnT0);
		if (strstr(cmdStr, "multi") != NULL)
			options.closeCD = 0;
		if (strstr(cmdStr, "offline") != NULL)
			options.burnOffline = 1;

		if (session->getFirstTrack() == NULL)
			throw "empty session";
		int makeImage = session->getFirstTrack()->isDirectory();

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

			try {
				if (options.medium == options.DVDplusRW) {
					toClient(hint, hintCDrecordStarted);
					if (options.format)
						run_DVDplusRW_format();
					run_growisofs();
				}
				else {
//				readATIP();
//				readTOC();

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

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

				toClient(hint, hintCDrecordStarted);
				run_cdrecord();
				}
				if (options.verify)
					verify();
			}
			catch (const char *msg) {
				try { readTOC(); } catch (const char *msg) {}
				logSession(makeImage? "S": "I", msg);
				throw msg;
			}

			logSession(makeImage? "S": "I", "OK");
//			try { readTOC(); } catch (const char *msg) {}
			if (config.getEjectOnDone())
				eject();
			if (options.copies > 1)
				log.put("cdrecord success (copy %d)", i + 1);
			else
				log.put("cdrecord success");
		}
		successToClient("cdrecord success");

		writers[writerNo].state = idle;
		resetImageFile();
		writerQueue->remove(serverNo);
	}
	catch (const char *msg) {
		if (hasWriter) {
			if (!noDisc() && config.getEjectOnDone())
				try { eject(); } catch (const char *msg) {}
			writers[writerNo].state = idle;
			resetImageFile();
		}
		writerQueue->remove(serverNo);
		throw msg;
	}
	catch (Error *error) {
		if (hasWriter) {
			if (!noDisc() && config.getEjectOnDone())
				try { eject(); } catch (const char *msg) {}
			writers[writerNo].state = idle;
			resetImageFile();
		}
		writerQueue->remove(serverNo);
		throw error;
	}
}

void Server::checkFileName(const char *fileName)
{
	if (strstr(fileName, "../") != NULL)
		throw new Error("filename contains ../");
}

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

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

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

void Server::ejectReader()
{
	Process p(config.getCdrecordPath());

	// add dev-argument of cdrecord
	char str[4*1024];
	sprintf(str, "dev=%s", config.getReader(readerNo));
	p.addArg(str);
	p.addArg("-eject");
	p.run();
}

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 = ::write(sock, lineStr, strlen(lineStr));
	openCommands--;
}

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];
	cmd[0] = arg1[0] = arg2[0] = arg3[0] = arg4[0] = 0;

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

	// if lineStr contains a password, crypt it
	{
		char *passwd = NULL;
		if (strcasecmp(cmd, "changePassword") == 0)
			passwd = arg1;
		else
		if (strcasecmp(cmd, "login") == 0)
			passwd = arg2;

		if (passwd != NULL)
		{
			strcpy(passwd, &crypt(passwd, "JH")[2]);
			char format[2 + argsNum * 3 + 1];
			strcpy(format, "%s");
			for (int i = 0; i < argsNum; i++)
				strcat(format, " %s");
			snprintf(lineStr, sizeof lineStr, format,
				cmd, arg1, arg2, arg3, arg4);
		}
	}

	// write lineStr to the logfiles
	if (strsNum > 0
		&& (strcasecmp(cmd, "putFile") == 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 Error("Anonymous use is not permitted.");

	// interpret the command
	if (project != NULL)
	{
		// save project mode commands
		if (strcasecmp(cmd, "add") == 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 Error("empty command");
	else
	if (strcasecmp(cmd, "blank") == 0)
		blank(lineStr);
	else
	if (strcasecmp(cmd, "burnCopy") == 0) {
		if (config.getCopySupport())
			burnCopy();
		else
			throw "not supported";
	}
	else
	if (strcasecmp(cmd, "burnSession") == 0)
	{
		checkOpenSession();
		try {
			burnSession(lineStr);
			delete session;
			session = NULL;
		}
		catch (const char *msg) {
			delete session;
			session = NULL;
			throw msg;
		}
	}
	else
	if (strcasecmp(cmd, "changePassword") == 0)
	{
		if (argsNum != 1)
			throw 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)
			throw new Error(errorInCommand);

		checkOpenSession();
		checkFileName(arg1);
		checkFileName(arg2);

		session->linkFile(
			File(config.getFtpPubDir(), arg1)
				.getName(), arg2);
		successToClient("file linked");
	}
	else
	if (strcasecmp(cmd, "linkTrack") == 0)
	{
		if (argsNum != 2)
			throw new Error(errorInCommand);

		checkOpenSession();
		checkFileName(arg1);

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

		checkFileName(dirName);
		File f(config.getFtpPubDir(), dirName);
		DIR *dir = opendir(f.getName());
		if (dir == NULL)
			throw new Error("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) {
				}
			else if (name.endsWith(".wav") || name.endsWith(".WAV"))
				try {
					Wave wave(f2);
					type = type + " WAV " + (int)wave.getSeconds();
				}
				catch (Exception *e) {
				}

			const char *name2 = spaceToEsc(name.getBytes());
			toClient(hint, "%s %d %s%s",
				f2.isDirectory()? "d": "f",
				f2.getSize(),
				name2,
				type.getBytes());
			delete[] name2;
		}
		closedir(dir);
		successToClient("list done");
	}
	else
	if (strcasecmp(cmd, "listProjects") == 0)
	{
		char str[1024];
		snprintf(str, sizeof str, "%s/%s",
			config.getHomeDir(), userID);
		DIR *dir = opendir(str);
		if (dir == NULL)
			throw new Error("Cannot open the directory %s.", str);

		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(str, 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 errorInCommand;

		checkLoggedIn();

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

		char str[1024];
		snprintf(str, sizeof str, "%s/%s/%s",
			config.getHomeDir(), userID, arg1);
		project = fopen(str, "r");
		if (project == NULL)
			throw new Error("Cannot open %s", 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 errorInCommand;

		if (options.protocolVersion >= 3)
		{
			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 Error(errorInCommand);

		checkOpenSession();
		checkFileName(dirName);

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

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

		if (strstr(arg1, "-") != NULL
			|| strstr(arg1, ":") != NULL)
			throw new Error("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 Error("Sending an eMail to %s failed.", arg2);
		}

		successToClient("added");
	}
	else
	if (strcasecmp(cmd, "newSession") == 0
		|| strcasecmp(cmd, "openSession") == 0)
	{
		if (argsNum > 1)
			throw 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());
		}

		if (argsNum == 0)
		{
			time_t t;
			time(&t);
			timeToShortString(t, arg1, sizeof arg1);
		}

		options.reset();
		options.setVolumeID(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 Error(errorInCommand);

		bytesReceived += length;
		try {
			checkOpenSession();
			checkFileName(fileName);
		}
		catch (Error *error) {
			skipBytes(inStream, length);
			throw error;
		}

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

		bytesReceived += length;
		try {
			checkOpenSession();
		}
		catch (Error *error) {
			skipBytes(inStream, length);
			throw error;
		}

		if (strcmp(trackType, "continue") != 0)
			session->newTrack(trackType);
		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, "resetOptions") == 0) {
		options.reset();
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "saveProject") == 0)
	{
		if (argsNum != 1)
			throw errorInCommand;

		checkLoggedIn();

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

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

		log.put("project %s started", str);
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "setBlankTOC") == 0)
	{
		options.blankTOC = 1;
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "setBootImage") == 0)
	{
		if (strsNum != 2)
			throw "setBootImage <filename>";
		checkFileName(arg1);
		char *ptr = arg1;
		while (*ptr == '/')
			ptr++;
		options.bootImage = strDup(ptr);
		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, "setFormat") == 0) {
		options.format = 1;
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "setHFS") == 0)
	{
		options.HFS = 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 (strsNum != 2)
			throw "setMedium [CDR | DVDplusRW | DVDplusR]";
		if (strcasecmp(arg1, "DVDplusRW") == 0)
			options.medium = options.DVDplusRW;
		else
		if (strcasecmp(arg1, "DVDplusR") == 0)
			options.medium = options.DVDplusR;
		else
			options.medium = options.CDR;
		successToClient("");
	}
	else
	if (strcasecmp(cmd, "setBurnOffline") == 0)
	{
		options.burnOffline = 1;
		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 "invalid";
	}
	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)
	{
		if (strsNum == 2
			&& sscanf(arg1, "%d", &options.speed) == 1
			&& 1 <= options.speed
			&& options.speed <= 50)
		{
			options.speed = min(options.speed,
				config.getMaxSpeed());
			successToClient("");
		}
		else
			errorToClient("missing or invalid argument");
	}
	else
	if (strcasecmp(cmd, "setVerify") == 0)
	{
		if (config.getVerify())
		{
			options.verify = 1;
			successToClient("");
		}
		else
			errorToClient("not supported");
	}
	else
	if (strcasecmp(cmd, "setVolumeID") == 0)
	{
		if (strsNum != 2)
			throw "setVolumeID <ID>";
		removeEscChars(arg1);
		options.setVolumeID(arg1);
		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
	{
		saveLineStr();
		throw new Error("unknown command");
	}
}

const char *Server::getNewLogFileName(char *str)
{
	char timeStr[80];
	sprintf(str, "%s/%s/%s.%03d",
		config.getLogDir(), userID,
		timeToDigits(connectionT0, timeStr, sizeof timeStr),
		++logFilesNum);
	return str;
}

void Server::initStatic()
{
	char *PATH = getenv("PATH");
	if (PATH == NULL)
		PATH = "";
	log.put("PATH=%s", getenv("PATH"));

	config.reload();
	configError = config.getFirstError() != NULL;
	store = NULL;
	status.start();
	if (!configError)
		initStatic2();
}

void Server::initStatic2()
{
	configError = config.getFirstError() != NULL;

	if (readerQueue == NULL)
		readerQueue = new Queue(config.getMaxOpenConnections());

	if (writerQueue == NULL)
		writerQueue = new Queue(config.getMaxOpenConnections());

	log.put("used CD-writer: \"%s\"", config.getWriterInfo(0));

	try {
		store = new Store();
	}
	catch (const char *msg)
	{
		log.put("%s", msg);
	}

	accounts = new Accounts();
}

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

void Server::logCDstate(const char *resultStr)
{
	static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
	pthread_mutex_lock(&mutex);

	char fileName[4*1024];
	sprintf(fileName, "%s/CDinfos", config.getLogDir());
	FILE *file = fopen(fileName, "a");
	if (file == NULL)
	{
		pthread_mutex_unlock(&mutex);
		return;
	}
	char str[1024];
	fprintf(file, "%s %s\n", CDstate.getLogStr(str), resultStr);
	fclose(file);
	pthread_mutex_unlock(&mutex);
}

void Server::logConnection()
{
	static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
	pthread_mutex_lock(&mutex);

	char fileName[4*1024];
	sprintf(fileName, "%s/connects", config.getLogDir());
	FILE *file = fopen(fileName, "a");
	if (file == NULL)
	{
		pthread_mutex_unlock(&mutex);
		return;
	}

	time_t t;
	time(&t);
	char timeStr[80];
	fprintf(file,
		"%s %-20s ",
		timeToString(connectionT0, timeStr, sizeof timeStr), userID);
	fprintf(file, "%s %10d\n",
		secondsToString(t - connectionT0, timeStr),
		bytesReceived);
	fclose(file);
	pthread_mutex_unlock(&mutex);
}

void Server::logSession(const char *typeStr, const char *resultStr)
{
	static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
	pthread_mutex_lock(&mutex);

	char resultStr2[strlen(resultStr) + 1];
	for (int i = 0; i <= strlen(resultStr); i++)
		if (resultStr[i] == ' ')
			resultStr2[i] = '_';
		else
			resultStr2[i] = resultStr[i];

	char fileName[4*1024];
	sprintf(fileName, "%s/sessions", config.getLogDir());
	FILE *file = fopen(fileName, "a");
	if (file == NULL)
	{
		pthread_mutex_unlock(&mutex);
		return;
	}

	time_t t;
	time(&t);
	char timeStr[80];
	fprintf(file,
		"%s %-20s ",
		timeToString(connectionT0, timeStr, sizeof timeStr), userID);
	fprintf(file, "%s ",
		secondsToString(sessionT1 - sessionT0, timeStr));
	fprintf(file, "%s ", secondsToString(t - burnT0, timeStr));
	int size;
//	if (session != NULL)
//		size = session->getSize();
//	else
		size = CDstate.getSize();
	fprintf(file, "%10d %s %s\n",
		size, typeStr, resultStr2);
	fclose(file);
	log.debug("SEGV0104", "3");

	if (strcasecmp(resultStr, "OK") != 0
		&& config.getEMailOnError() != NULL)
	{
		char cmdStr[4*1024];
		sprintf(cmdStr, "echo %s %s %s %s | mail -s %s %s",
			timeToString(connectionT0, timeStr, sizeof timeStr),
			userID, typeStr, resultStr,
			"CD-Writer-Error", config.getEMailOnError());
		system(cmdStr);
	}

	timeToString(t, timeStr, sizeof timeStr);
	char date[20], time2[20];
	strncpy(date, timeStr, 8);
	date[8] = 0;
	strncpy(time2, &timeStr[9], 9);
	time2[9] = 0;
	char user[strlen(clientUserName) + 1 + strlen(clientIP) + 1];
	sprintf(user, "%s@%s", clientUserName, clientIP);
	status.addResult(date, time2, user, size, resultStr);

	logCDstate(resultStr2);
	pthread_mutex_unlock(&mutex);
}

void Server::nextCmd()
{
	if (fgets(lineStr, sizeof lineStr, inStream) == 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 (Error *error) {
			errorToClient(error->toString());
			log.put("error: %s", error->toString());
			delete error;
		}
		catch (const char *msg) {
			errorToClient(msg);
			log.put("error: %s", msg);
		}
	else
	{
		openCommands++;
		errorToClient("command contains control-characters");
//		saveLineStr();
		quit = 1;
	}
}

int Server::noDisc(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()
{
	char str[4*1024];
	Cdrecord p(writerNo, &CDstate);
	p.setProtoFile(getNewLogFileName(str));
	p.addArg("-atip");
	p.run();
}

void Server::readMultiSessionInfo()
{
	Cdrecord p(writerNo);
	char logFile[4*1024];
	p.setProtoFile(getNewLogFileName(logFile));
	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 "invalid session offset";
	}
	else
	{
#ifndef FreeBSD
/*		// make sure the CD is really empty
		FILE *rawCDStream = fopen(
			config.getWriterRawDevice(writerNo), "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 "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()
{
	char str[4*1024];
	Cdrecord p(writerNo, &CDstate);
	p.setProtoFile(getNewLogFileName(str));
	p.addArg("-toc");

	p.start();
	while (1)
	{
		const char *lineStr = p.readLine();
		if (lineStr == NULL)
			break;
	}
	p.getResult();
//	CDstatus.isBlank = p.isBlankDisk();
}

int 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;
}

/**
 *	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;
			if (str[i] == 'u') {
				if (str[++i] == 0)
					break;
				int n = hextoint(&str[i], 4);
				if (n < 0)
					n = '?';
				else
					i += 3;

				if (n < 0x0080)
					str[j] = n;
				else
				if (n < 0x0100)
					str[j] = n;
				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 {
				int n = hextoint(&str[i], 2);
				if (n < 0)
					n = '?';
				else
					i += 1;

				str[j] = n;
			}
		}
		else
			str[j] = str[i];

		if (str[j] == 0)
			str[j] = '0';
	}

	str[j] = 0;
	return str;
}

void Server::reserveReader()
{
	int cancel = waitForService(
		readerQueue, config.getNumOfReaders() - 1);
	if (cancel)
		throw "waiting for reader cancelled";

	static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
	pthread_mutex_lock(&mutex);
	for (int i = 0; i < config.getNumOfReaders(); i++)
		if (readers[i].state == idle)
		{
			readers[i].state = reserved;
			readerNo = i;
			break;
		}
	pthread_mutex_unlock(&mutex);
}

void Server::reserveWriter()
{
	int cancel = waitForService(
		writerQueue, config.getNumOfWriters() - 1);
	if (cancel)
		throw "waiting for writer cancelled";

	static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
	pthread_mutex_lock(&mutex);
	for (int i = 0; i < config.getNumOfWriters(); i++)
		if (writers[i].state == idle)
		{
			writers[i].state = reserved;
			writerNo = i;
			break;
		}
	pthread_mutex_unlock(&mutex);
}

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

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

void Server::run_cdrdaoCopy() {
	writers[writerNo].fifoPercent = 100;
	writers[writerNo].percent = 0;
	writers[writerNo].state = writing;
	time(&burnT0);
	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");
	p.addArg("%d", options.speed);
	p.addArg("-v");
	p.addArg("2");
	p.addArg("--source-device");
	p.addArg(config.getReader(readerNo));
	p.addArg("--device");
	p.addArg(config.getWriter(writerNo));

	p.show();
	char str[4*1024];
	p.setProtoFile(getNewLogFileName(str));
	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)
				writers[writerNo].percent = 100;
			else
				writers[writerNo].percent = 100 * a / b;
			writers[writerNo].fifoPercent = p.getFifoPercent();
			break;
		}
	}
}

void Server::run_cdrecord()
{
	try {
		run_cdrecord2();
	}
	catch (const char *msg) {
		if (strcmp(msg, "restartWithoutMulti") == 0)
		{
			options.closeCD = 1;
			run_cdrecord2();
		}
		else
		if (strcmp(msg, "perhaps a DVD+RW") == 0)
			// for DVD+RW support
			writeImageToRawDevice();
		else
			throw msg;
	}
}

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

	// options to cdrecord
	addArgs(p, config.getCdrecordArgs());
	if (config.getBurnProof())
		p.addArg("driveropts=burnproof");
	if (options.laserOff || !config.getLaserOn())
		p.addArg("-dummy");
	if (!options.closeCD)
		p.addArg("-multi");
	p.addArg("speed=%d", options.speed);
	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");
		}
		p.addArg("-data");
		if (makeImageOnTheFly)
			p.addArg("-");
		else
			p.addArg(config.getImageFile(writerNo));
	}
	else
	{
		if (strstr(track->getName(), ".wav") == NULL)
			p.addArg("-data");
		else
		{
			p.addArg("-pad");
			if (options.DAO)
				p.addArg("-dao");
		}
		while (track != NULL)
		{
			p.addArg(track->getName());
			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;
		char str[1024];
		sprintf(str, "%s %d RR %d > /dev/null",
			config.getSetSchedulerPath(),
			mkisofs.getPID(), schedParam.sched_priority);
		if (mkisofs.getPID() > 0)
			if (sched_setscheduler(mkisofs.getPID(),
				SCHED_RR, &schedParam) == 0)
				;
			else
			// try external variant
			if (system(str) != 0)
				log.putv("Cannot set scheduler to RR %d",
					schedParam.sched_priority);

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

	p.show();
	char str[4*1024];
	p.setProtoFile(getNewLogFileName(str));
	p.start();
	int quit = 0;
	while (!quit)
	{
		fd_set fdSet;
		FD_ZERO(&fdSet);
		if (makeImageOnTheFly)
			FD_SET(mkisofs.getErrorStream(), &fdSet);
		FD_SET(p.getInputStream(), &fdSet);
		select(FD_SETSIZE, &fdSet, NULL, NULL, NULL);
		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 (const char *msg)
			{
				throw msg;
			}
			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)
					writers[writerNo].percent = 100;
				else
					writers[writerNo].percent = 100 * a / b;
				writers[writerNo].fifoPercent = p.getFifoPercent();
				break;

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

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

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

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

	p.addArg("blank=%s", modeStr);
//	p.addArg("-force");
	p.addArg("speed=%d", options.speed);
	p.addArg("-v");

	p.show();
	char str[4*1024];
	p.setProtoFile(getNewLogFileName(str));
	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 (Error *error) {
			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);
			writers[writerNo].percent = (int)percent;
			delete error;
		}
	}

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

	toClient(hint, "%s %f", hintPercentDone, 100);
	writers[writerNo].percent = 100;
}

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

	// options to cdrecord
	addArgs(p, config.getCdrecordArgs());
	if (config.getBurnProof())
		p.addArg("driveropts=burnproof");
	if (options.laserOff || !config.getLaserOn())
		p.addArg("-dummy");
	if (!options.closeCD)
		p.addArg("-multi");
	p.addArg("speed=%d", options.speed);
	p.addArg("-v");
	p.addArg("-isosize");
	p.addArg(config.getReaderRawDevice(readerNo));

	p.show();
	char str[4*1024];
	p.setProtoFile(getNewLogFileName(str));
	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)
				writers[writerNo].percent = 100;
			else
				writers[writerNo].percent = 100 * a / b;
			writers[writerNo].fifoPercent = p.getFifoPercent();
			break;

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

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

void Server::run_mkisofs()
{
	writers[writerNo].percent = 0;
	writers[writerNo].state = mkisofs;
	log.putv("Server::run_mkisofs %s",
		session->getFirstTrack()->getName());

	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 (Error *error) {
			delete error;
			p.terminate();
			throw "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);
				writers[writerNo].percent = (int)percent;
			}
		}
	}

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

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::showWaitForDiscMsg()
{
	FILE *inStream = fopen(config.getWaitForCDMsgFile(), "r");
	if (inStream == NULL)
		toClient(info, "Please insert a CD-R(W)!");
	else
	{
		while (1)
		{
			char str[1024];
			fgets(str, sizeof str, inStream);
			if (feof(inStream))
				break;
			replace(str, "%d", config.getWaitForCD());
			replace(str, "$reservedMinutes",
				config.getWaitForCD());
			replace(str, "$writerNo", writerNo + 1);
			replace(str, "$writerType",
				config.getWriterInfo(writerNo));
			toClient(info, str);
		}
		fclose(inStream);
	}
}

const char *Server::spaceToEsc(const char *str)
{
	char str2[2 * strlen(str) + 1];
	int i, j;
	for (i = 0, j = 0; str[i] != 0; i++, j++)
	{
		if (str[i] == ' ')
		{
			str2[j++] = '\\';
			str2[j] = '_';
		}
		else
		if (str[i] == '\\')
		{
			str2[j++] = '\\';
			str2[j] = '\\';
		}
		else
			str2[j] = str[i];
	}
	str2[j] = 0;
	return strDup(str2);
}

void Server::start_mkisofs(Process &p)
{
	if (!CDstate.isLastSessionInfoValid()
			&& options.medium == options.CDR)
		readMultiSessionInfo();

	char str[4*1024];
	p.setProtoFile(getNewLogFileName(str));

	// arguments for mkisofs
	addArgs(p, config.getMkisofsArgs());
	if (options.bootImage != NULL)
	{
		p.addArg("-eltorito-boot");
		p.addArg(options.bootImage);
	}
	if (options.HFS)
		p.addArg("-hfs");
	if (options.joliet)
		p.addArg("-joliet");
	p.addArg("-follow-links");
	p.addArg("-full-iso9660-filenames");
//	p.addArg("-allow-leading-dots");
	if (options.rockRidge)
		p.addArg("-rational-rock");
	p.addArg("-verbose");
	p.addArg("-gui");
	if (!config.getImageOnTheFly())
	{
		p.addArg("-o");
		p.addArg(config.getImageFile(writerNo));
	}
	if (options.volumeID != NULL)
	{
		p.addArg("-volid");
		p.addArg(options.volumeID);
	}
	p.addArg("-p");
	char timeStr[80];
	snprintf(str, sizeof str, "Org: %s, User: %s, %s, %s %s",
		config.getOrganization(), userID,
		timeToString(sessionT1, timeStr, sizeof timeStr),
		product, version);
	p.addArg(str);

	if (CDstate.lastSession1 > 0
			&& options.medium == options.CDR) {
		p.addArg("-C");
		sprintf(str, "%d,%d",
			CDstate.lastSession0, CDstate.lastSession1);
		p.addArg(str);
		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.getName());

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

	p.start(session->getFirstTrack()->getName(),
		config.getImageOnTheFly() && options.medium == options.CDR);
}

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 = ::write(sock, lineStr, strlen(lineStr));
	openCommands--;
}

void Server::terminate()
{
}

char *Server::timeToDigits(time_t t, char *str, size_t size)
{
	struct tm *tm = localtime(&t);
	snprintf(str, size, "%4d%02d%02d%02d%02d%02d",
		1900 + tm->tm_year, tm->tm_mon + 1, tm->tm_mday,
		tm->tm_hour, tm->tm_min, tm->tm_sec);
	return str;
}

char *Server::timeToShortString(time_t t, char *str, size_t size)
{
	struct tm *tm = localtime(&t);
	snprintf(str, size, "%4d%02d%02d_%02d%02d",
		1900 + tm->tm_year, tm->tm_mon + 1, tm->tm_mday,
		tm->tm_hour, tm->tm_min);
	return str;
}

char *Server::timeToString(time_t t, char *str, size_t size)
{
	struct tm *tm = localtime(&t);
	snprintf(str, size, "%4d%02d%02d %02d:%02d:%02d",
		1900 + tm->tm_year, tm->tm_mon + 1, tm->tm_mday,
		tm->tm_hour, tm->tm_min, tm->tm_sec);
	return str;
}

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 = ::write(sock, lineStr, strlen(lineStr));
}

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

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

	writers[writerNo].percent = 0;
	writers[writerNo].state = verifying;
	toClient(hint, hintVerifying);

	eject();
	load();

	Process p(config.getVerifyPath());
	if (session != NULL)
		p.addArg(session->getFirstTrack()->getName());
	else
		p.addArg(config.getReaderRawDevice(readerNo));

	p.addArg(config.getWriterRawDevice(writerNo));
	p.show();
	char str[4*1024];
	p.setProtoFile(getNewLogFileName(str));
	p.start();

	int numOfErrors = 0;
	while (1)
	{
		const char *lineStr = p.readLine();
		if (lineStr == NULL)
			break;
		if (strlen(lineStr) > 0)
			toClient("CDWverify", "%s", lineStr);

		int MBytes;
		if (sscanf(lineStr, "%d", &MBytes) == 1)
		{
			int size = 0;
//			if (session != NULL)
//				size = session->getSize();
//			else
				size = CDstate.getSize();
			if (size <= 0)
				size = 100 * 1024 * 1024;
			int percent = (100 * 1024 * MBytes) / (size / 1024);
			toClient(hint, "%s %d", hintPercentDone, percent);
			writers[writerNo].percent = percent;
		}
		else
		if (strstr(lineStr, "error:") == lineStr)
		{
			log.putv("%s", lineStr);
			numOfErrors++;
		}
	}

	if (numOfErrors > 0)
	{
		sprintf(errorStr, "%d verify errors", numOfErrors);
		throw errorStr;
	}
}

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

	showWaitForDiscMsg();

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

	log.put("writer reserved - waiting for CD");
	writers[writerNo].owner = this;
	writers[writerNo].state = reserved;

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

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

		gettimeofday(&t, NULL);
		timeout = t.tv_sec > writers[writerNo].reservedUntil;
	}

	if (cancel || timeout)
		load();

	if (cancel)
		throw "reservation cancelled";
	else
	if (timeout)
		throw "reservation timeout";
	else
	{
		toClient(hint, hintCDdetected);
		CDstate.reset();
		return 0;
	}
}

int Server::waitForService(Queue *queue, int toRank)
{
	log.put("Server::waitForService %d", toRank);

	// wait for rank <= toRank or cancel by new command on socket
	int rank, oldRank;
	rank = queue->getRank(serverNo);
	oldRank = rank + 1;
	int cancel = 0;
	while (rank > toRank && !cancel)
	{
		if (rank < oldRank)
			toClient(hint, "rank %d", rank - toRank);
		cancel = waitForInput(inStream, 1) != 0;
		oldRank = rank;
		rank = queue->getRank(serverNo);
	}

	char str[1024];
	if (cancel)
		strcpy(str, "cancelled");
	else
		sprintf(str, "%d <= %d", rank, toRank);

	log.put("Server::waitForService %s", str);
	return cancel;
}

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

	writers[writerNo].fifoPercent = 100;
	writers[writerNo].percent = 0;
	writers[writerNo].state = writing;
	time(&burnT0);
	log.putv("Server::writeImageToRawDevice");

	if (!config.getImageOnTheFly())
		throw "Set imageOnTheFly=on for DVD+RW support";

	const File *track = session->getFirstTrack();
	if (!track->isDirectory())
		throw "Cannot make an ISO image for a DVD+RW";

	int rawDevice = open(config.getWriterRawDevice(writerNo), O_WRONLY);
	if (rawDevice == -1)
		throw "Cannot open raw device";

	Process mkisofs(config.getMkisofsPath());
	start_mkisofs(mkisofs);

	int maxBlocks = 2295104;
	toClient(hint, "capacity %d %d %d",
		maxBlocks, maxBlocks, maxBlocks - CDstate.imageSizeInBlocks);

	int i = 0;
	int quit = 0;
	while (!quit)
	{
		fd_set fdSet;
		FD_ZERO(&fdSet);
		FD_SET(mkisofs.getErrorStream(), &fdSet);
		FD_SET(mkisofs.getInputStream(), &fdSet);
		select(FD_SETSIZE, &fdSet, NULL, NULL, NULL);
		if (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(mkisofs.getInputStream(), &fdSet))
			continue;

		char buf[2048];
		int n;
		while ((n = ::read(mkisofs.getInputStream(), buf, sizeof buf)) == -1
			&& errno == EINTR)
			;

		if (n == 0)
			break;

		int m;
		while ((m = ::write(rawDevice, buf, n)) == -1
			&& errno == EINTR)
			;

		if (m < n)
			throw "Cannot write image to raw device";
		if (i++%512 != 0)
			continue;

		int a = i / 512, b = CDstate.imageSizeInBlocks / 512;
		toClient(hint, "Track 01: %d of %d", a, b);
		if (b == 0)
			writers[writerNo].percent = 100;
		else
			writers[writerNo].percent = 100 * a / b;
	}
}
