#include "config.h"
#include <QtXml>
#include <QDir>
#include <QApplication>
#include <QProcess>
#include <QDate>
#include <QThread>
#include <QStringList>
#include <QIcon>
#include <QMessageBox>
#include <QHostInfo> // available since Qt 4.7
#include <QList>
#include <QRegularExpression>
#include <unistd.h> // for getuid()
#include <sys/types.h> // for getuid(), getpwuid()
#include <pwd.h> // for getpwuid()
#include "globals.h"
#include "vsession.h"
#include "sessionsiconholder.h"
#include "userldapdata.h"
#include "virtualizer.h"
static const QString VMWARE("vmware");
static const QString VIRTUALBOX("virtualbox");
struct IconMap {
QString icon;
QRegularExpression expr;
IconMap(const QString &icon, const QString ®exp)
: icon(icon), expr(regexp) {}
};
// It's first match wins, so put a check for windows95 before windows9
static const QList<IconMap> iconMapping(
{
IconMap("dos", "^(ms)?dos"),
IconMap("win311", "^win(dows)?3($|1)"),
IconMap("win9x", "^win(dows)?(95|98|me|nt)"),
IconMap("winxp", "^win(dows)?(xp|2003|2k3)"),
IconMap("win2000", "^win(dows)?(2000|2k)"), // After win2k3!
IconMap("win7", "^win(dows)?7"),
IconMap("win8", "^win(dows)?8"),
IconMap("win10", "^win(dows)?(9|10)"), // After win95!
IconMap("osx", "^darwin"),
IconMap("suse", "^opensuse"),
});
// These match via .startsWith and need to exist as a resource with this exact name
static const QStringList directOsNames(
{
"amiga", "atari",
"beos",
"debian",
"fedora", "freebsd",
"gentoo",
"macos",
"opensolaris", "os2", "osx",
"redhat", "riscos",
"solaris", "suse",
"ubuntu",
});
static const QRegularExpression cleanNameRegex("[^a-z0-9]");
QString tmpFileName;
bool VSession::init(const QDomElement& xml) {
QDomElement settingsNode = this->doc_.createElement("settings");
this->doc_.appendChild(settingsNode);
eintrag_ = this->doc_.importNode(xml, true).toElement();
return !settingsNode.appendChild(eintrag_).isNull() && !settingsNode.isNull();
}
void VSession::addNodeWithAttribute(const QString& nodeName,
const QString& value,
const QString& attribute,
bool replace) {
QDomElement node =
eintrag_.firstChildElement(nodeName);
if (replace == false || node.isNull()) {
// create a new node
node = this->doc_.createElement(nodeName);
eintrag_.appendChild(node);
}
node.setAttribute(attribute, value);
}
QIcon VSession::icon() const {
QString icon(getAttribute(QStringLiteral("icon")));
SessionsIconHolder *iconHolder = SessionsIconHolder::get();
if (icon.startsWith(QStringLiteral("http://")) || icon.startsWith(QStringLiteral("https://"))) {
// try to load icon from url
QIcon url_icon(iconHolder->getIcon(QUrl(icon)));
if (!url_icon.isNull())
return url_icon;
}
if (!icon.isEmpty()) {
// See if we have a resource with this name, or if it's an absolute path
QIcon res_icon(iconHolder->getIcon(icon));
if (!res_icon.isNull())
return res_icon;
}
// Everything failed, try to guess the OS
QString os1(this->os().toLower());
QString os2(this->osDisplayName().toLower());
os1 = os1.replace(cleanNameRegex, QString());
os2 = os2.replace(cleanNameRegex, QString());
if (!os1.isEmpty() || !os2.isEmpty()) {
// Now try known good OS names via .startsWith()
for (const QString& str : directOsNames) {
if (os1.startsWith(str) || os2.startsWith(str))
return iconHolder->getIcon(str);
}
// Fuzzy matching via regex
for (const IconMap& map : iconMapping) {
if (map.expr.match(os1).hasMatch() || map.expr.match(os2).hasMatch())
return iconHolder->getIcon(map.icon);
}
// Running out of ideas...
if (os2.startsWith(QStringLiteral("win")))
return iconHolder->getIcon(QStringLiteral("windows"));
if (os2.contains(QStringLiteral("linux")))
return iconHolder->getIcon(QStringLiteral("linux"));
}
// Fallback to generic virtualizer icon (if found)
QIcon ret = iconHolder->getIcon(virtualizer());
if (ret.isNull()) {
ret = QIcon(QPixmap(64, 64));
}
return ret;
}
QString VSession::toXml() const {
QDomDocument doc(doc_);
QDomNode xmlNode = doc.createProcessingInstruction(
"xml", "version=\"1.0\" encoding=\"UTF-8\"");
doc.insertBefore(xmlNode, doc.firstChild());
QString image(this->getAttribute(QStringLiteral("image_name")));
QDomElement path = doc.firstChildElement(QStringLiteral("settings"))
.firstChildElement(QStringLiteral("eintrag"))
.appendChild(doc.createElement(QStringLiteral("image_path")))
.toElement();
if (QFileInfo(image).isRelative()) {
// make path to image absolute
path.setAttribute(QStringLiteral("param"), Config::get(Config::BASEDIR) + "/" + image);
} else {
path.setAttribute(QStringLiteral("param"), image);
}
return doc.toString();
}
QString VSession::getAttribute(const QString &nodeName,
const QString &attribute) const {
return eintrag_.firstChildElement(nodeName).attribute(attribute);
}
QList<QString> VSession::keywords() const {
return this->keywords_;
}
void VSession::readKeywords() {
QDomNode keywordsNode = eintrag_.namedItem(QStringLiteral("keywords"));
for (QDomElement el(keywordsNode.firstChildElement(QStringLiteral("keyword")));
!el.isNull();
el = el.nextSiblingElement(QStringLiteral("keyword"))) {
this->keywords_.append(el.text());
}
}
bool VSession::containsKeywords(const QList<QString>& keywords) const {
for (int j = 0; j < keywords.length(); ++j) {
if (!this->shortDescription().contains(keywords[j], Qt::CaseInsensitive)
&& !this->description().contains(keywords[j], Qt::CaseInsensitive)
&& !this->getAttribute(QStringLiteral("creator"), QStringLiteral("param")).contains(keywords[j], Qt::CaseInsensitive)) {
bool match = false;
for (int i = 0; i < this->keywords().length(); ++i) {
if (this->keywords()[i].contains(keywords[j], Qt::CaseInsensitive)) {
match = true;
break;
}
}
if (!match)
return false;
}
}
return true;
}
QString VSession::getNodeText(const QString& nodeName) const {
return this->doc_.namedItem(nodeName).toText().data();
}
bool VSession::isActive() const {
QString value(getAttribute(QStringLiteral("active")));
// Is disabled completely
if (value.compare("false") == 0) {
if (g_debugMode) qDebug() << "'" << shortDescription() << "' not active. Reason: active == false";
return false;
}
// Filter by LDAP data
if (!UserLdapData::isEmpty()) {
QDomElement el(eintrag_.namedItem(QStringLiteral("filters")).firstChildElement(QStringLiteral("filter")));
if (!el.isNull()) {
for (; !el.isNull(); el = el.nextSiblingElement(QStringLiteral("filter"))) {
if (el.attribute(QStringLiteral("type")) != QStringLiteral("LDAP"))
continue;
if (UserLdapData::isAllowed(el.firstChildElement(QStringLiteral("key")).text(), el.firstChildElement(QStringLiteral("value")).text()))
return true;
}
return false;
}
}
return true;
}
bool VSession::isLocked() const {
// default to false
return getAttribute(QStringLiteral("locked")).compare(QStringLiteral("true")) == 0;
}
QString VSession::checkCanRunInternal() const {
if (getAttribute(QStringLiteral("image_name")).isEmpty())
return QObject::trUtf8("XML error: image_name is empty");
const Virtualizer* virt = Virtualizer::get(virtualizer());
if (!virt->isAvailable)
return QObject::trUtf8("Virtualizer '%1' is not enabled.").arg(virt->id);
// Seems OK
return QString();
}
int VSession::priority() const {
int prio = getAttribute(QStringLiteral("priority")).toInt();
if (g_templateHandling == TEMPLATES_BUMP && isTemplate()) {
prio -= 500;
}
if (g_forLocationHandling != LOCATION_IGNORE && isForLocation()) {
prio -= 1000;
}
return prio;
}
bool VSession::prepareRun() const {
if (!canRun(true))
return false;
// write xml to temporary file
QTemporaryFile tmpfile(QDir::tempPath() + QStringLiteral("/vmchooser-XXXXXX.xml"));
if (!tmpfile.open() ||
tmpfile.write(this->toXml().toUtf8()) == -1) {
qDebug() << "Error writing xml to file" << tmpfile.fileName();
QMessageBox::critical(
nullptr, QObject::trUtf8("vmchooser"),
QObject::trUtf8("Error writing temporary XML file for run-virt"));
return false;
}
if (!tmpFileName.isEmpty()) {
QFile(tmpFileName).remove();
}
// Docs say we should call fileName() before closing, to prevent deletion
tmpFileName = tmpfile.fileName();
tmpfile.setAutoRemove(false);
tmpfile.close();
if (g_noVtx && needsVtx()) {
QMessageBox::StandardButton ret = QMessageBox::question(nullptr, QObject::tr("Warning"),
QObject::tr("The selected session is based on a 64 bit operating system,"
" but this computer doesn't seem to support this (VT-x/AMD-V not"
" supported by CPU, or disabled in BIOS). You will probably get an"
" error message while the virtualizer is initializing.\n\n"
"Do you still want to try to start this VM?"),
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
if (ret != QMessageBox::Yes)
return false;
}
return true;
}
void VSession::run() const {
if (g_debugMode) {
qDebug() << "Sarting session " << shortDescription() << " ...";
}
QByteArray fn = tmpFileName.toUtf8();
QByteArray script = Config::get(Config::RUNSCRIPT).toUtf8();
char *argv[3] = { script.data(), fn.data(), nullptr };
execv(script.data(), argv);
}
int VSession::type() const {
return Session::VSESSION;
}
QList<Session*> VSession::loadFromXmlDocument(const QDomDocument& doc) {
QList<Session*> sessionList;
if (doc.isNull())
return sessionList;
UserLdapData::init();
QDomElement settingsNode = doc.firstChildElement(QStringLiteral("settings"));
if (settingsNode.isNull())
return sessionList;
for (QDomElement el(settingsNode.firstChildElement(QStringLiteral("eintrag")));
!el.isNull();
el = el.nextSiblingElement(QStringLiteral("eintrag"))) {
VSession* e = new VSession;
if (e->init(el) && e->isActive()) {
e->readKeywords();
sessionList.append(e);
} else {
delete e;
}
}
return sessionList;
}
bool VSession::needsVtx() const {
QString type = virtualizer();
return (type == VMWARE && os().endsWith(QStringLiteral("-64")))
|| (type == VIRTUALBOX && os().endsWith(QStringLiteral("_64"))); // Vbox 6.x DOES support 32bit VMs without VT-x, but if
// the config enables any feature that cannot work without VT-x, it will silently enable it and then fail to start (i.e. IOAPIC)
// TODO: qemu-kvm, ...
}
QVariant VSession::foregroundRole() const {
if ((!g_noVtx || !needsVtx()) && canRun())
return Session::foregroundRole();
return QColor(180, 180, 180);
}
bool VSession::operator<(const Session& other) const {
int p0 = this->priority();
int p1 = other.priority();
if (p0 < p1) return true;
if (p0 == p1) {
QString d0 = this->shortDescription();
QString d1 = other.shortDescription();
return d0.localeAwareCompare(d1) < 0;
}
return false;
}