/*
 * This file is part of DesktopBSD, see the README file.
 *
 * Author: Peter Hofer <ph@desktopbsd.net>
 *         (C) 2004, 2005
 *
 * Copyright: BSD, see the COPYING file included in this distribution
 *
 */

#include <qregexp.h>

#include <kvm.h>
#include <sys/param.h>
#include <sys/sysctl.h>
#include <sys/user.h>
#include <fcntl.h>
#include <limits.h> /* _POSIX2_LINE_MAX */
#include <signal.h> /* killpg() */

#include "pkgupgradecontroller.h"

PkgUpgradeController::PkgUpgradeController(QObject * parent, const char * name)
	:	QObject(parent, name),
		processID(-1),
		overallProgress(-1)
{
}

void PkgUpgradeController::processChanged()
{
	processID = -1;
}

void PkgUpgradeController::outputReceived(const QString & data) {
	QStringList lines = QStringList::split('\n', data, false);

	for(QStringList::Iterator i = lines.begin(); i != lines.end(); ++i) {
		const QString & line = *i;

		int reMatch = -1;
		if(line.startsWith("--->  ") || line.startsWith("===> ")) {
			emit progressMessage(line.mid(4).stripWhiteSpace());
			recalculateProgress();
		} else if(line.startsWith("** "))
			emit warningMessage(line.mid(3));
		else if(line.startsWith("++ ") || line.startsWith("=> "))
			emit informationMessage(line.mid(3));
		else if(line.startsWith("-*-"))
			emit userInputRequired(line.mid(3));
		else if(line.contains('%') && (reMatch = QRegExp("..\\d%").search(line)) != -1) {
			/* fetch download progress */
			bool success;
			uint percent = line.mid(reMatch, 3).toUInt(&success);
	
			if(success)
				emit operationProgressChanged(percent);
		}
	}
}


/**
  * @brief Find a running portupgrade process.
  *
  * The process must be a child of the calling program.
  * When the process is found, process id and process
  * group id are stored in processID and processGroupID.
  */
void PkgUpgradeController::findPortupgradeProcess()
{
	char buffer[_POSIX2_LINE_MAX];
	const char *execf = "/dev/null", *coref = execf;
	kvm_t *kd = kvm_openfiles(execf, coref, NULL, O_RDONLY, buffer);

	if(kd != NULL)
	{
		if(processID == -1)
		{
			int	count;
			pid_t	ownPID = getpid();
	
			kinfo_proc *pinfo = kvm_getprocs(kd, KERN_PROC_PROC, 0, &count);
	
			for(int i = 0; i < count; ++i)
			{
				if(pinfo->ki_ppid == ownPID)
				{
					char **args = kvm_getargv(kd, pinfo, 0);
					if(args != NULL && (strstr(args[0], ": portupgrade: ") != NULL
						|| (args[1] != NULL && !strcmp(args[1], "/usr/local/sbin/portupgrade"))))
					{
						processID = pinfo->ki_pid;
					}
				}
	
				++pinfo;
			}
		}

		kvm_close(kd);
	}
}


/**
  * Extract the overall progress from the portupgrade process's
  * first argument which could look like this:
  * "ruby18: portupgrade: [2/5] editors/koffice-kde3"
  *
  * Emit overallProgressChanged(int) if the process in percent
  * differs from the value in the last event.
  */
void PkgUpgradeController::recalculateProgress() {
	if(processID < 0)
		findPortupgradeProcess();

	if(processID >= 0)
	{
		char *arg0 = NULL;

		char buffer[_POSIX2_LINE_MAX];
		const char *execf = "/dev/null", *coref = execf;
		kvm_t *kd = kvm_openfiles(execf, coref, NULL, O_RDONLY, buffer);

		int count;
		kinfo_proc *pinfo = kvm_getprocs(kd, KERN_PROC_PID, processID, &count);
		if(count == 1) {
			char **args = kvm_getargv(kd, pinfo, 0);
			if(args != NULL)
				arg0 = args[0];
		}
	
		if(arg0 != NULL && strstr(arg0, ": portupgrade: ") != NULL)
		{
			/* sl[2]: progress indicator, e.g. "[12/34]". sl[3]: Current package, e.g. "openoffice.org-2.0.2" */
			QStringList sl = QStringList::split(' ', arg0);

			QString	sCurrent = sl[2].section('/', 0, 0).mid(1),	/* "12" */
						sTotal = sl[2].section('/', 1, 1);				/* "34]" */
		
			sTotal.truncate(sTotal.length() - 1);	/* "34" */

			int total = sTotal.toInt();
			if(total != 0)
			{
				int percent = (sCurrent.toInt() - 1) * 100 / total;
				if(percent != overallProgress)
				{
					overallProgress = percent;
					emit overallProgressChanged(percent);
				}
			}

			if(sl[3] != currentPackage)
			{
				currentPackage = sl[3];
				emit currentPackageChanged(currentPackage);
			}
		}

		kvm_close(kd);
	}
}


/** Sends SIGTERM: @see PkgUpgradeController::killGroup(int) */
int PkgUpgradeController::killAll() {
	return killAll(SIGTERM);
}


/**
  * Sends a signal to all processes in the portupgrade process's
  * process group (usually all processes in the same terminal).
  * 
  * @param signal See FreeBSD manual page signal(3)
  * 
  * @return -1 if kvm_openfiles() failed, -2 if the portupgrade process
  *         couldn't be found.
  */
int PkgUpgradeController::killAll(int signal) {
	if(processID < 0)
		findPortupgradeProcess();

	if(processID >= 0) {
		char buffer[_POSIX2_LINE_MAX];
		const char *execf = "/dev/null", *coref = execf;
		kvm_t *kd = kvm_openfiles(execf, coref, NULL, O_RDONLY, buffer);
		if(kd != NULL) {
			int pcount;
			kinfo_proc *pinfo = kvm_getprocs(kd, KERN_PROC_PROC, 0, &pcount);

			killAllRecursive(pinfo, pcount, processID, signal);
	
			kvm_close(kd);

			return 0;
		}

		return -1;
	}

	return -2;
}


/**
  * Kills all children of a process recursively, starting with
  * the leaf processes (without further children).
  * 
  * @param pinfo A pointer to the start of a struct kinfo_proc list.
  */
void PkgUpgradeController::killAllRecursive(kinfo_proc *pinfo, int pcount, pid_t process, int signal) {
	for(int i = 0; i < pcount; ++i)
	{
		if(pinfo[i].ki_ppid == process)
			killAllRecursive(pinfo, pcount, pinfo[i].ki_pid, signal);
	}

	kill(process, signal);
}
