Mercurial > hg > easyhg
view src/hgrunner.cpp @ 678:c0b46d0514a7 scale
Incomplete scaling updates. Should do this differently
author | Chris Cannam |
---|---|
date | Thu, 06 Dec 2018 14:55:45 +0000 |
parents | 4283398d248f |
children | 5b3bcb2d0943 |
line wrap: on
line source
/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ /* EasyMercurial Based on HgExplorer by Jari Korhonen Copyright (c) 2010 Jari Korhonen Copyright (c) 2013 Chris Cannam Copyright (c) 2013 Queen Mary, University of London This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. See the file COPYING included with this distribution for more information. */ #include "hgrunner.h" #include "common.h" #include "debug.h" #include "settingsdialog.h" #include <QSettings> #include <QInputDialog> #include <QStandardPaths> #include <QTemporaryFile> #include <QDir> #include <QProgressBar> #include <QPushButton> #include <QGridLayout> #include <iostream> #include <errno.h> #include <stdio.h> #include <stdlib.h> #ifndef Q_OS_WIN32 #include <unistd.h> #include <termios.h> #include <fcntl.h> #else #include <process.h> #endif HgRunner::HgRunner(QString myDirPath, QWidget *parent) : QWidget(parent), m_myDirPath(myDirPath) { QGridLayout *layout = new QGridLayout(this); layout->setMargin(0); m_progress = new QProgressBar; layout->addWidget(m_progress, 0, 0); m_cancel = new QPushButton; m_cancel->setIcon(QIcon(":images/cancel-small.png")); m_cancel->setFlat(true); m_cancel->setFixedHeight(m_progress->sizeHint().height()); m_cancel->setFixedWidth(m_progress->sizeHint().height()); connect(m_cancel, SIGNAL(clicked()), this, SLOT(killCurrentActions())); layout->addWidget(m_cancel, 0, 1); m_proc = 0; // Always unbundle the extension: even if it already exists (in // case we're upgrading) and even if we're not going to use it (so // that it's available in case someone wants to use it later, // e.g. to fix a malfunctioning setup). But the path we actually // prefer is the one in the settings first, if it exists; then the // unbundled one; then anything in the path if for some reason // unbundling failed unbundleExtension(); m_progress->setTextVisible(false); hide(); m_isRunning = false; } HgRunner::~HgRunner() { closeTerminal(); if (m_proc) { m_proc->kill(); m_proc->deleteLater(); } if (m_authFilePath != "") { QFile(m_authFilePath).remove(); } //!!! and remove any other misc auth file paths... } QString HgRunner::getUnbundledFileName() { return SettingsDialog::getUnbundledExtensionFileName(); } QString HgRunner::unbundleExtension() { // Pull out the bundled Python file into a temporary file, and // copy it to our known extension location, replacing the magic // text NO_EASYHG_IMPORT_PATH with our installation location QString bundled = ":easyhg.py"; QString unbundled = getUnbundledFileName(); QString target = QFileInfo(unbundled).path(); if (!QDir().mkpath(target)) { DEBUG << "Failed to make unbundle path " << target << endl; std::cerr << "Failed to make unbundle path " << target << std::endl; return ""; } QFile bf(bundled); DEBUG << "unbundle: bundled file will be " << bundled << endl; if (!bf.exists() || !bf.open(QIODevice::ReadOnly)) { DEBUG << "Bundled extension is missing!" << endl; return ""; } QTemporaryFile tmpfile(QString("%1/easyhg.py.XXXXXX").arg(target)); tmpfile.setAutoRemove(false); DEBUG << "unbundle: temp file will be " << tmpfile.fileName() << endl; if (!tmpfile.open()) { DEBUG << "Failed to open temporary file " << tmpfile.fileName() << endl; std::cerr << "Failed to open temporary file " << tmpfile.fileName() << std::endl; return ""; } QString all = QString::fromUtf8(bf.readAll()); all.replace("NO_EASYHG_IMPORT_PATH", m_myDirPath); tmpfile.write(all.toUtf8()); DEBUG << "unbundle: wrote " << all.length() << " characters" << endl; tmpfile.close(); QFile ef(unbundled); if (ef.exists()) { DEBUG << "unbundle: removing old file " << unbundled << endl; ef.remove(); } DEBUG << "unbundle: renaming " << tmpfile.fileName() << " to " << unbundled << endl; if (!tmpfile.rename(unbundled)) { DEBUG << "Failed to move temporary file to target file " << unbundled << endl; std::cerr << "Failed to move temporary file to target file " << unbundled << std::endl; return ""; } DEBUG << "Unbundled extension to " << unbundled << endl; return unbundled; } void HgRunner::requestAction(HgAction action) { DEBUG << "requestAction " << action.action << ": " << m_queue.size() << " thing(s) in queue, current action is " << m_currentAction.action << endl; bool pushIt = true; action = expandEnvironment(action); if (m_queue.empty()) { if (action == m_currentAction) { // this request is identical to the thing we're executing DEBUG << "requestAction: we're already handling this one, ignoring identical request" << endl; pushIt = false; } } else { HgAction last = m_queue.back(); if (action == last) { // this request is identical to the previous thing we // queued which we haven't executed yet DEBUG << "requestAction: we're already queueing this one, ignoring identical request" << endl; pushIt = false; } } if (pushIt) { m_queue.push_back(action); } checkQueue(); } HgAction HgRunner::expandEnvironment(HgAction action) { // Adjust the executable and params for action to match our actual // environment. We do this when the action is received, rather // than when we execute it, so that we can compare // (post-expansion) commands to see e.g. whether the one just // received is the same as the one we're currently executing QString executable = action.executable; QStringList params = action.params; if (executable == "") { // This is a Hg command executable = getHgBinaryName(); if (executable == "") executable = "hg"; QString ssh = getSshBinaryName(); if (ssh != "") { params.push_front(QString("ui.ssh=\"%1\"").arg(ssh)); params.push_front("--config"); } if (action.mayBeInteractive()) { params.push_front("ui.interactive=true"); params.push_front("--config"); QSettings settings; if (settings.value("useextension", true).toBool()) { params = addExtensionOptions(params); } } } action.executable = executable; action.params = params; return action; } QString HgRunner::getHgBinaryName() { QSettings settings; settings.beginGroup("Locations"); return settings.value("hgbinary", "").toString(); } QString HgRunner::getSshBinaryName() { QSettings settings; settings.beginGroup("Locations"); return settings.value("sshbinary", "").toString(); } QString HgRunner::getExtensionLocation() { QSettings settings; settings.beginGroup("Locations"); QString extpath = settings.value("extensionpath", "").toString(); if (extpath != "" && QFile(extpath).exists()) return extpath; return ""; } void HgRunner::started() { DEBUG << "started" << endl; /* m_proc->write("blah\n"); m_proc->write("blah\n"); m_proc -> closeWriteChannel(); */ } void HgRunner::noteUsername(QString name) { m_userName = name; } void HgRunner::noteRealm(QString realm) { m_realm = realm; } void HgRunner::getUsername() { if (m_ptyFile) { bool ok = false; QString prompt = tr("User name:"); if (m_realm != "") { prompt = tr("User name for \"%1\":").arg(m_realm); } QString pwd = QInputDialog::getText (qobject_cast<QWidget *>(parent()), tr("Enter user name"), prompt, QLineEdit::Normal, QString(), &ok); if (ok) { m_ptyFile->write(QString("%1\n").arg(pwd).toUtf8()); m_ptyFile->flush(); return; } else { DEBUG << "HgRunner::getUsername: user cancelled" << endl; killCurrentCommand(); return; } } // user cancelled or something went wrong DEBUG << "HgRunner::getUsername: something went wrong" << endl; killCurrentCommand(); } void HgRunner::getPassword() { if (m_ptyFile) { bool ok = false; QString prompt = tr("Password:"); if (m_userName != "") { if (m_realm != "") { prompt = tr("Password for \"%1\" at \"%2\":") .arg(m_userName).arg(m_realm); } else { prompt = tr("Password for user \"%1\":") .arg(m_userName); } } QString pwd = QInputDialog::getText (qobject_cast<QWidget *>(parent()), tr("Enter password"), prompt, QLineEdit::Password, QString(), &ok); if (ok) { m_ptyFile->write(QString("%1\n").arg(pwd).toUtf8()); m_ptyFile->flush(); return; } else { DEBUG << "HgRunner::getPassword: user cancelled" << endl; killCurrentCommand(); return; } } // user cancelled or something went wrong DEBUG << "HgRunner::getPassword: something went wrong" << endl; killCurrentCommand(); } bool HgRunner::checkPrompts(QString chunk) { //DEBUG << "checkPrompts: " << chunk << endl; if (!m_currentAction.mayBeInteractive()) return false; QString text = chunk.trimmed(); QString lower = text.toLower(); if (lower.endsWith("password:")) { getPassword(); return true; } if (lower.endsWith("user:") || lower.endsWith("username:")) { getUsername(); return true; } QRegExp userRe("\\buser(name)?:\\s*([^\\s]+)"); if (userRe.indexIn(text) >= 0) { noteUsername(userRe.cap(2)); } QRegExp realmRe("\\brealmr:\\s*([^\\s]+)"); if (realmRe.indexIn(text) >= 0) { noteRealm(realmRe.cap(1)); } return false; } void HgRunner::dataReadyStdout() { DEBUG << "dataReadyStdout" << endl; if (!m_proc) return; QString chunk = QString::fromUtf8(m_proc->readAllStandardOutput()); if (!checkPrompts(chunk)) { m_stdout += chunk; } } void HgRunner::dataReadyStderr() { DEBUG << "dataReadyStderr" << endl; if (!m_proc) return; QString chunk = QString::fromUtf8(m_proc->readAllStandardError()); DEBUG << chunk; if (!checkPrompts(chunk)) { m_stderr += chunk; } } void HgRunner::dataReadyPty() { DEBUG << "dataReadyPty" << endl; QString chunk = QString::fromUtf8(m_ptyFile->readAll()); DEBUG << "chunk of " << chunk.length() << " chars" << endl; if (!checkPrompts(chunk)) { m_stdout += chunk; } } void HgRunner::error(QProcess::ProcessError) { finished(-1, QProcess::CrashExit); } void HgRunner::finished(int procExitCode, QProcess::ExitStatus procExitStatus) { if (!m_proc) return; // Save the current action and reset m_currentAction before we // emit a signal to mark the completion; otherwise we may be // resetting the action after a slot has already tried to set it // to something else to start a new action HgAction completedAction = m_currentAction; DEBUG << "HgRunner::finished: completed " << completedAction.action << endl; m_isRunning = false; m_currentAction = HgAction(); //closeProcInput(); m_proc->deleteLater(); m_proc = 0; if (completedAction.action == ACT_NONE) { DEBUG << "HgRunner::finished: WARNING: completed action is ACT_NONE" << endl; } else { if (procExitCode == 0 && procExitStatus == QProcess::NormalExit) { DEBUG << "HgRunner::finished: Command completed successfully" << endl; // DEBUG << "stdout is " << m_stdout << endl; emit commandCompleted(completedAction, m_stdout); } else { DEBUG << "HgRunner::finished: Command failed, exit code " << procExitCode << ", exit status " << int(procExitStatus) << ", stderr follows" << endl; DEBUG << m_stderr << endl; emit commandFailed(completedAction, m_stderr, m_stdout); } } checkQueue(); } void HgRunner::killCurrentActions() { HgAction current = m_currentAction; m_queue.clear(); killCurrentCommand(); emit commandCancelled(current); } void HgRunner::killCurrentCommand() { if (m_isRunning) { m_currentAction.action = ACT_NONE; // so that we don't bother to notify if (m_proc) m_proc->kill(); } } void HgRunner::checkQueue() { if (m_isRunning) { return; } if (m_queue.empty()) { hide(); return; } HgAction toRun = m_queue.front(); m_queue.pop_front(); DEBUG << "checkQueue: have action: running " << toRun.action << endl; startCommand(toRun); } void HgRunner::pruneOldAuthFiles() { QString path = QStandardPaths::writableLocation (QStandardPaths::CacheLocation); QDir d(path); if (!d.exists()) return; QStringList filters; filters << "easyhg.*.dat"; QStringList fl = d.entryList(filters); foreach (QString f, fl) { QStringList parts = f.split('.'); if (parts.size() > 1) { int pid = parts[1].toInt(); DEBUG << "Checking pid " << pid << " for cache file " << f << endl; ProcessStatus ps = GetProcessStatus(pid); if (ps == ProcessNotRunning) { DEBUG << "Removing stale cache file " << f << endl; QDir(d).remove(f); } } } } QString HgRunner::getAuthFilePath() { if (m_authFilePath == "") { pruneOldAuthFiles(); QByteArray fileExt = randomKey(); if (fileExt == QByteArray()) { DEBUG << "HgRunner::getAuthFilePath: Failed to get proper auth file ext" << endl; return ""; } QString fileExt16 = QString::fromLocal8Bit(fileExt.toBase64()).left(16) .replace('+', '-').replace('/', '_'); QString path = QStandardPaths::writableLocation (QStandardPaths::CacheLocation); QDir().mkpath(path); if (path == "") { DEBUG << "HgRunner::getAuthFilePath: Failed to get cache location" << endl; return ""; } m_authFilePath = QString("%1/easyhg.%2.%3.dat").arg(path) .arg(getpid()).arg(fileExt16); } return m_authFilePath; } QString HgRunner::getAuthKey() { if (m_authKey == "") { QByteArray key = randomKey(); if (key == QByteArray()) { DEBUG << "HgRunner::getAuthKey: Failed to get proper auth key" << endl; return ""; } QString key16 = QString::fromLocal8Bit(key.toBase64()).left(16); m_authKey = key16; } return m_authKey; } QStringList HgRunner::addExtensionOptions(QStringList params) { QString extpath = getExtensionLocation(); if (extpath == "") { DEBUG << "HgRunner::addExtensionOptions: Failed to get extension location" << endl; return params; } QString afp = getAuthFilePath(); QString afk = getAuthKey(); if (afp != "" && afk != "") { params.push_front(QString("easyhg.authkey=%1").arg(m_authKey)); params.push_front("--config"); params.push_front(QString("easyhg.authfile=%1").arg(m_authFilePath)); params.push_front("--config"); } // Looks like this one must be without quotes, even though the SSH // one above only works on Windows if it has quotes (at least where // there is a space in the path). Odd params.push_front(QString("extensions.easyhg=%1").arg(extpath)); params.push_front("--config"); return params; } void HgRunner::startCommand(HgAction action) { if (action.workingDir.isEmpty()) { // We require a working directory, never just operate in pwd emit commandFailed(action, "EasyMercurial: No working directory supplied, will not run Mercurial command without one", ""); return; } m_isRunning = true; m_progress->setRange(0, 0); if (!action.shouldBeFast()) { show(); m_cancel->setVisible(action.makesSenseToCancel()); } m_stdout.clear(); m_stderr.clear(); m_realm = ""; m_userName = ""; m_proc = new QProcess; QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); #ifdef Q_OS_WIN32 // On Win32 we like to bundle Hg and other executables with EasyHg if (m_myDirPath != "") { env.insert("PATH", m_myDirPath + ";" + env.value("PATH")); } #endif #ifdef Q_OS_MAC if (QSettings().value("python32", false).toBool()) { env.insert("VERSIONER_PYTHON_PREFER_32_BIT", "1"); } #endif env.insert("LANG", "en_US.utf8"); env.insert("LC_ALL", "en_US.utf8"); env.insert("HGENCODING", "utf8"); env.insert("HGPLAIN", "1"); m_proc->setProcessEnvironment(env); connect(m_proc, SIGNAL(started()), this, SLOT(started())); connect(m_proc, SIGNAL(error(QProcess::ProcessError)), this, SLOT(error(QProcess::ProcessError))); connect(m_proc, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(finished(int, QProcess::ExitStatus))); connect(m_proc, SIGNAL(readyReadStandardOutput()), this, SLOT(dataReadyStdout())); connect(m_proc, SIGNAL(readyReadStandardError()), this, SLOT(dataReadyStderr())); m_proc->setWorkingDirectory(action.workingDir); if (action.mayBeInteractive()) { openTerminal(); if (m_ptySlaveFilename != "") { DEBUG << "HgRunner: connecting to pseudoterminal" << endl; m_proc->setStandardInputFile(m_ptySlaveFilename); // m_proc->setStandardOutputFile(m_ptySlaveFilename); // m_proc->setStandardErrorFile(m_ptySlaveFilename); } } QString cmdline = action.executable; foreach (QString param, action.params) cmdline += " " + param; DEBUG << "HgRunner: starting: " << cmdline << " with cwd " << action.workingDir << endl; m_currentAction = action; DEBUG << "set current action to " << m_currentAction.action << endl; emit commandStarting(action); m_proc->start(action.executable, action.params); } void HgRunner::closeProcInput() { DEBUG << "closeProcInput" << endl; if (m_proc) m_proc->closeWriteChannel(); } void HgRunner::openTerminal() { #ifndef Q_OS_WIN32 if (m_ptySlaveFilename != "") return; // already open DEBUG << "HgRunner::openTerminal: trying to open new pty" << endl; int master = posix_openpt(O_RDWR | O_NOCTTY); if (master < 0) { DEBUG << "openpt failed" << endl; perror("openpt failed"); return; } struct termios t; if (tcgetattr(master, &t)) { DEBUG << "tcgetattr failed" << endl; perror("tcgetattr failed"); } cfmakeraw(&t); if (tcsetattr(master, TCSANOW, &t)) { DEBUG << "tcsetattr failed" << endl; perror("tcsetattr failed"); } if (grantpt(master)) { perror("grantpt failed"); } if (unlockpt(master)) { perror("unlockpt failed"); } char *slave = ptsname(master); if (!slave) { perror("ptsname failed"); ::close(master); return; } m_ptyMasterFd = master; m_ptyFile = new QFile(); connect(m_ptyFile, SIGNAL(readyRead()), this, SLOT(dataReadyPty())); if (!m_ptyFile->open(m_ptyMasterFd, QFile::ReadWrite)) { DEBUG << "HgRunner::openTerminal: Failed to open QFile on master fd" << endl; } m_ptySlaveFilename = slave; DEBUG << "HgRunner::openTerminal: succeeded, slave is " << m_ptySlaveFilename << endl; #endif } void HgRunner::closeTerminal() { #ifndef Q_OS_WIN32 if (m_ptySlaveFilename != "") { delete m_ptyFile; m_ptyFile = 0; ::close(m_ptyMasterFd); m_ptySlaveFilename = ""; } #endif }