diff src/hgrunner.cpp @ 370:b9c153e00e84

Move source files to src/
author Chris Cannam
date Thu, 24 Mar 2011 10:27:51 +0000
parents hgrunner.cpp@bb189827f6d1
children 5cc0d897eb26
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/hgrunner.cpp	Thu Mar 24 10:27:51 2011 +0000
@@ -0,0 +1,544 @@
+/* -*- 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) 2011 Chris Cannam
+    Copyright (c) 2011 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 <QPushButton>
+#include <QListWidget>
+#include <QDialog>
+#include <QLabel>
+#include <QVBoxLayout>
+#include <QSettings>
+#include <QInputDialog>
+#include <QTemporaryFile>
+#include <QDir>
+
+#include <iostream>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#ifndef Q_OS_WIN32
+#include <unistd.h>
+#include <termios.h>
+#include <fcntl.h>
+#endif
+
+HgRunner::HgRunner(QString myDirPath, QWidget * parent) :
+    QProgressBar(parent),
+    m_myDirPath(myDirPath)
+{
+    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();
+
+    setTextVisible(false);
+    setVisible(false);
+    m_isRunning = false;
+}
+
+HgRunner::~HgRunner()
+{
+    closeTerminal();
+    if (m_proc) {
+        m_proc->kill();
+        m_proc->deleteLater();
+    }
+}
+
+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 << endl;
+    bool pushIt = true;
+    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();
+}
+
+QString HgRunner::getHgBinaryName()
+{
+    QSettings settings;
+    settings.beginGroup("Locations");
+    return settings.value("hgbinary", "").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;
+    QString chunk = QString::fromUtf8(m_proc->readAllStandardOutput());
+    if (!checkPrompts(chunk)) {
+        m_stdout += chunk;
+    }
+}
+
+void HgRunner::dataReadyStderr()
+{
+    DEBUG << "dataReadyStderr" << endl;
+    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)
+{
+    // 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;
+
+    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 " << procExitStatus
+                  << ", stderr follows" << endl;
+            DEBUG << m_stderr << endl;
+            emit commandFailed(completedAction, m_stderr);
+        }
+    }
+
+    checkQueue();
+}
+
+void HgRunner::killCurrentActions()
+{
+    m_queue.clear();
+    killCurrentCommand();
+}
+
+void HgRunner::killCurrentCommand()
+{
+    if (m_isRunning) {
+        m_currentAction.action = ACT_NONE; // so that we don't bother to notify
+        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::startCommand(HgAction action)
+{
+    QString executable = action.executable;
+    bool interactive = false;
+    QStringList params = action.params;
+
+    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;
+    }
+
+    QSettings settings;
+    settings.beginGroup("General");
+
+    if (executable == "") {
+        // This is a Hg command
+        executable = getHgBinaryName();
+
+        if (action.mayBeInteractive()) {
+            params.push_front("ui.interactive=true");
+            params.push_front("--config");
+
+            if (settings.value("useextension", true).toBool()) {
+                QString extpath = getExtensionLocation();
+                params.push_front(QString("extensions.easyhg=%1").arg(extpath));
+                params.push_front("--config");
+            }
+            interactive = true;
+        }            
+
+        //!!! want an option to use the mercurial_keyring extension as well
+    }
+
+    m_isRunning = true;
+    setRange(0, 0);
+    if (!action.shouldBeFast()) show();
+    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 (settings.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("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 (interactive) {
+        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 = executable;
+    foreach (QString param, params) cmdline += " " + param;
+    DEBUG << "HgRunner: starting: " << cmdline << " with cwd "
+          << action.workingDir << endl;
+
+    m_currentAction = action;
+
+    // fill these out with what we actually ran
+    m_currentAction.executable = executable;
+    m_currentAction.params = params;
+
+    DEBUG << "set current action to " << m_currentAction.action << endl;
+    
+    emit commandStarting(action);
+
+    m_proc->start(executable, params);
+}
+
+void HgRunner::closeProcInput()
+{
+    DEBUG << "closeProcInput" << endl;
+
+    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
+}