
// Project: webCDwriter - Network CD Writing
// File: Server.cpp
// Author: 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 "Cdrecord.h"
#include "Hints.h"
#include "Support.h"
#include "Process.h"
#include "Version.h"
#include "Log.h"
#include "Config.h"
#include "Queue.h"
#include "Session.h"
#include "Server.h"
#include "Store.h"
#include "Status.h"

const char
	hint[] = "HINT",
	info[] = "INFO",
	*unknown = "unknown";

void readMultiSessionInfo(const char *logFileName, int &a, int &b);
char *removeEscChars(char *str);
const char *timeToDigits(time_t t, char *str);
const char *timeToString(time_t t, char *str);

Server::Server(int socket)
{
	time(&connectionT0);
	char timeStr[80];
	log.put("%s started", timeToString(connectionT0, 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("peerName <%s> port = %d\n",
				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("setsockopt SO_KEEPALIVE: %s", strerror(errno));

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

	// modes
	modeShowCdrecord = 0;
	modeShowMkisofs = 0;

	session = NULL;
	quit = 0;

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

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

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

	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.getStatusPortNo());
		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::fifoPercent = 100;
int Server::nextServerNo = 1;
Server *Server::writerOwner = NULL;
Queue *Server::writerQueue = NULL;
enum writerStates Server::writerStatus = idle;
int Server::writerPercent = 0;
int Server::writerReservedUntil = 0;
int Server::configError = 0;

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

	time_t t;
	time(&t);
	char timeStr[80];
	log.put("%s terminated", timeToString(t, timeStr));
	logConnection();

	if (session != NULL)
	{
		delete session;
		session = NULL;
	}

	if (clientIP != unknown)
		delete clientIP;

	activeServersNum--;
}

void Server::addCdrecordArgs(Process &p)
{
	const char *args = config.getCdrecordArgs();
	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();

		int cancel = waitForService(writerQueue, 0);
		if (cancel)
			throw "waiting for writer cancelled";

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

		writerPercent = 100;
		try { readTOC(); } catch (const char *msg) {}
		eject();
		log.put("cdrecord success");
		successToClient("cdrecord success");

		writerStatus = idle;
		writerQueue->remove();
	}
	catch (const char *msg) {
		if (hasWriter)
		{
			if (!noDisc())
				try { eject(); } catch (const char *msg) {}
			writerStatus = idle;
		}
		writerQueue->remove();
		throw msg;
	}
}

void Server::burnSession(char *cmdStr)
{
	// end of transfer
	time(&sessionT1);
	int hasWriter = 0;
	try {
		writerQueue->add();

		int cancel = waitForService(writerQueue, 0);
		if (cancel)
			throw "waiting for writer cancelled";

		hasWriter = 1;
		time(&burnT0);
		strcat(cmdStr, " ");
		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();

		waitForDisc();

		try {
//			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();
		}
		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) {}
		eject();
		log.put("cdrecord success");
		successToClient("cdrecord success");

		writerStatus = idle;
		resetImageFile();
		writerQueue->remove();
	}
	catch (const char *msg) {
		if (hasWriter)
		{
			if (!noDisc())
				try { eject(); } catch (const char *msg) {}
			writerStatus = idle;
			resetImageFile();
		}
		writerQueue->remove();
		throw msg;
	}
}

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

void Server::eject()
{
	Cdrecord p;
	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 str1[2 * sizeof lineStr],
		str2[2 * sizeof lineStr], str3[2 * sizeof lineStr];
	int strsNum = sscanf(lineStr, "%s %s %s", str1, str2, str3);

	// write lineStr to the logfiles
	if (strsNum > 0
		&& (strcasecmp(str1, "putFile") == 0
			|| strcasecmp(str1, "mkdir") == 0))
		log.putv("command: <%s>", lineStr);
	else
		log.put("command: <%s>", lineStr);

	// interpret the command
	if (strlen(lineStr) == 0)
		throw "empty command";
	else
	if (strcasecmp(str1, "blank") == 0)
		blank(lineStr);
	else
	if (strcasecmp(str1, "burnSession") == 0)
	{
		checkOpenSession();
		try {
			burnSession(lineStr);
			delete session;
			session = NULL;
		}
		catch (const char *msg) {
			delete session;
			session = NULL;
			throw msg;
		}
	}
	else
	if (strcasecmp(str1, "client") == 0)
	{
		successToClient("");
	}
	else
	if (strcasecmp(str1, "closeSession") == 0)
	{
//		checkOpenSession();
		if (session != NULL)
			delete session;
		session = NULL;
		successToClient("");
	}
	else
	if (strcasecmp(str1, "login") == 0)
	{
		if (strsNum == 3)
		{
			if (userID == unknown)
				userID = strDup(str2);
			userID2 = strDup(str2);
			userPassword = strDup(crypt(str3, "JH"));
			successToClient("LOGIN completed");
		}
		else
			errorToClient("in command %s", str1);
	}
	else
	if (strcasecmp(str1, "mkdir") == 0)
	{
		checkOpenSession();
		char fileName[sizeof lineStr];
		int mode, time;
		sscanf(lineStr, "mkdir %s %o %d",
			fileName, &mode, &time);
		removeEscChars(fileName);

		session->makeDir(fileName, mode, time);
		successToClient("Verzeichnis angelegt");
	}
	else
	if (strcasecmp(str1, "newSession") == 0
		|| strcasecmp(str1, "openSession") == 0)
	{
		if (strsNum <= 2)
		{
			int len = strlen(userID2)
				+ 1 + strlen(userPassword);
			if (strsNum == 2)
				len += 1 + strlen(str2);

			char sessionID[len + 1];
			sprintf(sessionID, "%s-%s",
				userID2, &userPassword[2]);
			if (strsNum == 2)
				strcat(sessionID, str2);

			try
			{
				if (session != NULL)
					delete session;
				session = new Session(this, sessionID);
				if (strcasecmp(str1, "openSession") == 0)
					session->open();
				if (session->getFilesNum() > 0)
					toClient(hint, "files %d",
						session->getFilesNum());
				else
				if (session->getTracksNum() > 0)
					toClient(hint, "files %d",
						session->getTracksNum());
				options.reset();
				successToClient("");
				time(&sessionT0);
			}
			catch (const char *msg)
			{
				log.put(msg);
				errorToClient(msg);
			}
		}
		else
			errorToClient("in command %s", str1);
	}
	else
	if (strcasecmp(str1, "noop") == 0)
		successToClient("");
	else
	if (strcasecmp(str1, "putFile") == 0)
	{
		checkOpenSession();
		char fileName[2 * sizeof lineStr];
		int length, mode, time;
		if (sscanf(lineStr, "%s %s %d %o %d",
			str1, fileName, &length, &mode, &time) != 5)
			throw "error in command";
		removeEscChars(fileName);
		bytesReceived += length;
		session->putFile(inStream, fileName, length, mode, time);
		successToClient("file stored");
	}
	else
	if (strcasecmp(str1, "putTrack") == 0)
	{
		checkOpenSession();
		char trackType[1024];
		int length;
		if (sscanf(lineStr, "%s %s %d",
			str1, trackType, &length) != 3)
			throw "error in command";
		bytesReceived += length;
		if (strcmp(trackType, "continue") != 0)
			session->newTrack(trackType);
		session->putTrack(inStream, length);
		successToClient("file stored");
	}
	else
	if (strcasecmp(str1, "quit") == 0)
	{
		log.put("connection terminated by client");
		successToClient("");
		quit = 1;
	}
	else
	if (strcasecmp(str1, "setBlankTOC") == 0)
	{
		options.blankTOC = 1;
		successToClient("");
	}
	else
	if (strcasecmp(str1, "setBootImage") == 0)
	{
		if (strsNum != 2)
			throw "setBootImage <filename>";
		removeEscChars(str2);
		char *ptr = str2;
		while (*ptr == '/')
			ptr++;
		options.bootImage = strDup(ptr);
		successToClient("");
	}
	else
	if (strcasecmp(str1, "setCloseCD") == 0)
	{
		options.closeCD = 1;
		successToClient("");
	}
	else
	if (strcasecmp(str1, "setHFS") == 0)
	{
		options.HFS = 1;
		successToClient("");
	}
	else
	if (strcasecmp(str1, "setJoliet") == 0)
	{
		options.joliet = 1;
		successToClient("");
	}
	else
	if (strcasecmp(str1, "setLaserOff") == 0)
	{
		options.laserOff = 1;
		successToClient("");
	}
	else
	if (strcasecmp(str1, "setBurnOffline") == 0)
	{
		options.burnOffline = 1;
		successToClient("");
	}
	else
	if (strcasecmp(str1, "setProtocol") == 0)
	{
		if (strsNum == 2
			&& sscanf(str2, "%d",
				&options.protocolVersion) == 1)
		{
			if (options.protocolVersion >= 2)
			{
				modeShowCdrecord = 1;
				modeShowMkisofs = 1;
			}
			successToClient("");
		}
		else
			throw "invalid";
	}
	else
	if (strcasecmp(str1, "setRockRidge") == 0)
	{
		options.rockRidge = 1;
		successToClient("");
	}
	else
	if (strcasecmp(str1, "setShowCdrecord") == 0)
	{
		modeShowCdrecord = 1;
		successToClient("");
	}
	else
	if (strcasecmp(str1, "setShowMkisofs") == 0)
	{
		modeShowMkisofs = 1;
		successToClient("");
	}
	else
	if (strcasecmp(str1, "setSpeed") == 0)
	{
		if (strsNum == 2
			&& sscanf(str2, "%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(str1, "setVolumeID") == 0)
	{
		if (strsNum != 2)
			throw "setVolumeID <ID>";
		removeEscChars(str2);
		options.volumeID = strDup(str2);
		successToClient("");
	}
	else
	if (strcasecmp(str1, "system") == 0)
	{
		if (strsNum >= 2)
			clientSystemName = strDup(str2);
		if (strsNum >= 3)
			clientSystemVersion = strDup(str3);
		successToClient("");
	}
	else
	if (strcasecmp(str1, "user") == 0)
	{
		if (strsNum >= 2 && strcasecmp(str2, "noID") != 0)
			clientUserID = strDup(str2);
		if (strsNum >= 3)
			clientUserName = strDup(str3);
		if (userID == unknown)
		{
			char *str = new char[
				strlen(clientSystemName)
				+ 1 + strlen(clientUserID)
				+ 1 + strlen(clientUserName) + 1];
			if (clientUserID == unknown)
				sprintf(str, "%s-%s", clientSystemName,
					clientUserName);
			else
				sprintf(str, "%s-%s-%s", clientSystemName,
					clientUserID, clientUserName);
			userID = str;
		}
		successToClient("");
	}
	else
	if (strcasecmp(str1, "vm") == 0)
	{
		successToClient("");
	}
	else
	{
		saveLineStr();
		throw "unknown command";
	}
}

const char *Server::getNewLogFileName(char *str)
{
	char timeStr[80];
	sprintf(str, "%s/%s/%s.%03d",
		config.getLogDir(), userID,
		timeToDigits(connectionT0, 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 (writerQueue == NULL)
		writerQueue = new Queue(config.getMaxOpenConnections());

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

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

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

void Server::logCDstatus(const char *resultStr)
{
	char fileName[4*1024];
	sprintf(fileName, "%s/CDinfos", config.getLogDir());
	FILE *file = fopen(fileName, "a");
	if (file == NULL)
		return;
	char str[1024];
	fprintf(file, "%s %s\n", CDstatus.getLogStr(str), resultStr);
	fclose(file);
}

void Server::logConnection()
{
	char fileName[4*1024];
	sprintf(fileName, "%s/connects", config.getLogDir());
	FILE *file = fopen(fileName, "a");
	if (file == NULL)
		return;

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

void Server::logSession(const char *typeStr, const char *resultStr)
{
	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)
		return;

	time_t t;
	time(&t);
	char timeStr[80];
	fprintf(file,
		"%s %-20s ",
		timeToString(connectionT0, timeStr), userID);
	fprintf(file, "%s ",
		secondsToString(sessionT1 - sessionT0, timeStr));
	fprintf(file, "%s ", secondsToString(t - burnT0, timeStr));
	fprintf(file, "%10d %s %s\n",
		session->getSize(), typeStr, resultStr2);
	fclose(file);

	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),
			userID, typeStr, resultStr,
			"CD-Writer-Error", config.getEMailOnError());
		system(cmdStr);
	}

	timeToString(t, 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, session->getSize(), resultStr);

	logCDstatus(resultStr2);
}

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 (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;
		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(&CDstatus);
	p.setProtoFile(getNewLogFileName(str));
	p.addArg("-atip");
	p.run();
}

void Server::readTOC()
{
	char str[4*1024];
	Cdrecord p(&CDstatus);
	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();
}

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

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

void Server::run_cdrecord()
{
	try {
		run_cdrecord2();
	}
	catch (const char *msg) {
		if (strcmp(msg, "restartWithoutMulti") == 0)
		{
			options.closeCD = 1;
			run_cdrecord2();
		}
		else
			throw msg;
	}
}

void Server::run_cdrecord2()
{
	fifoPercent = 100;
	writerPercent = 0;
	writerStatus = writing;
	time(&burnT0);
	log.putv("Server::run_cdrecord");
	Cdrecord p(&CDstatus);

	// options to cdrecord
	addCdrecordArgs(p);
	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;
	const File *track = session->getFirstTrack();
	if (track->isDirectory())
	{
		if (config.getImageOnTheFly())
		{
			makeImageOnTheFly = 1;
			p.addArg("tsize=%ds", CDstatus.imageSizeInBlocks);
			p.addArg("-waiti");
		}
		p.addArg("-data");
		if (makeImageOnTheFly)
			p.addArg("-");
		else
			p.addArg(config.getImageFile());
	}
	else
	{
		if (strstr(track->getName(), ".wav") == NULL)
			p.addArg("-data");
		else
			p.addArg("-pad");
		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",
					CDstatus.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)
					writerPercent = 100;
				else
					writerPercent = 100 * a / b;
				fifoPercent = p.getFifoPercent();
				break;
			}

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

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

void Server::run_cdrecordBlank(const char *modeStr)
{
	writerPercent = 0;
	writerStatus = blanking;
	log.putv("Server::run_cdrecordBlank");
	char str[4*1024];
	Cdrecord p(&CDstatus);
	p.setProtoFile(getNewLogFileName(str));
	sprintf(str, "blank=%s", modeStr);
	p.addArg(str);
	p.addArg("-force");
	sprintf(str, "speed=%d", options.speed);
	p.addArg(str);
	p.addArg("-v");
	p.show();
	if (p.run() != 0)
		if (p.getFirstError() == NULL)
			throw "unknown cdrecord error";
		else
			throw p.getFirstError();
}

void Server::run_mkisofs()
{
	writerPercent = 0;
	writerStatus = 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 = p.readLine();
		if (lineStr == NULL)
			break;
		toClient("MKISOFS", "%s", lineStr);

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

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

		if (strstr(str, " extents written") == str)
		{
			CDstatus.extentsWrittenKB = 2 * n;
			log.put("mkisofs extentsWritten = %d MB",
				CDstatus.extentsWrittenKB / 1024);
		}
		if (*str == ' ')
			toClient(hint, "KByte %ld", 2 * n);
		if (*str == '.')
		{
			float percent;
			if (sscanf(lineStr, "%f", &percent) == 1)
			{
				toClient(hint,
					"mkisofsPercentDone %f", percent);
				writerPercent = (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,
			"The CD-writer was reserved for %d minutes.",
			config.getWaitForCD());
		toClient(info,
			"Insert a CD and wait until it is ejected!");
	}
	else
	{
		while (1)
		{
			char str[1024];
			fgets(str, sizeof str, inStream);
			if (feof(inStream))
				break;
			toClient(info, str, config.getWaitForCD());
		}
		fclose(inStream);
	}
}

void Server::start_mkisofs(Process &p)
{
	char str[4*1024], msinfoLogFile[4*1024];
	getNewLogFileName(msinfoLogFile);
	p.setProtoFile(getNewLogFileName(str));

	// arguments for mkisofs
	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("-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());
	}
	if (options.volumeID != NULL)
	{
		p.addArg("-volid");
		p.addArg(options.volumeID);
	}
	p.addArg("-p");
	char timeStr[80];
	sprintf(str, "Org: %s, User: %s, %s, %s %s",
		config.getOrganization(), userID,
		timeToString(sessionT1, timeStr),
		product, version);
	p.addArg(str);

	try {
		int a, b;
		readMultiSessionInfo(msinfoLogFile, a, b);
		if (b < a)
			throw "b < a";
		p.addArg("-C");
		sprintf(str, "%d,%d", a, b);
		p.addArg(str);
		p.addArg("-M");
		p.addArg(config.getCdrecordDevArg());
		toClient(hint, "msinfo %d %d", 2*a, 2*b);
	}
	catch (const char *msg) {
		if (strstr(msg, "first writable address") != NULL)
			throw msg;

#ifndef FreeBSD
		// make sure the CD is really empty
		FILE *rawCDStream = fopen(config.getMkisofsMArg(), "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
		if (strcmp(msg, "b < a") == 0)
			throw "wrong session offset";
		log.putv(msg);
		toClient(hint, "msinfo 0 0");
	}

	// 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());
}

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()
{
}

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

int Server::waitForDisc()
{
	eject();

	showWaitForDiscMsg();

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

	log.put("writer reserved - waiting for CD");
	writerOwner = this;
	writerStatus = reserved;

	struct timeval t;
	gettimeofday(&t, NULL);
	writerReservedUntil = 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 > writerReservedUntil;
	}

	if (cancel || timeout)
		load();

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

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

	// wait for rank <= toRank or cancel by new command on socket
	int rank, oldRank;
	rank = queue->getRank();
	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();
	}

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

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


void readMultiSessionInfo(const char *logFileName, int &a, int &b)
{
	Cdrecord p;
	p.setProtoFile(logFileName);
	p.addArg("-msinfo");

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

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

	if (n != 2)
		throw "can't read msinfo";
}

/**
 *	"\_" in " " und "\"ch in ch umwandeln
*/
char *removeEscChars(char *str)
{
	int i, j;
	for (i = 0, j = 0; str[i] != 0; i++, j++)
	{
		if (str[i] == '\\')
		{
			i++;
			if (str[i] == '_')
				str[i] = ' ';
		}
		str[j] = str[i];
	}
	str[j] = 0;
	return str;
}

const char *timeToDigits(time_t t, char *str)
{
	struct tm *tm = localtime(&t);
	sprintf(str, "%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;
}

const char *timeToString(time_t t, char *str)
{
	struct tm *tm = localtime(&t);
	sprintf(str, "%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;
}
