Chris@57: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ Chris@57: Chris@57: /* Chris@57: EasyMercurial Chris@57: Chris@57: Based on HgExplorer by Jari Korhonen Chris@57: Copyright (c) 2010 Jari Korhonen Chris@244: Copyright (c) 2011 Chris Cannam Chris@244: Copyright (c) 2011 Queen Mary, University of London Chris@57: Chris@57: This program is free software; you can redistribute it and/or Chris@57: modify it under the terms of the GNU General Public License as Chris@57: published by the Free Software Foundation; either version 2 of the Chris@57: License, or (at your option) any later version. See the file Chris@57: COPYING included with this distribution for more information. Chris@57: */ jtkorhonen@0: jtkorhonen@0: #include "hgrunner.h" Chris@62: #include "common.h" Chris@57: #include "debug.h" Chris@240: #include "settingsdialog.h" Chris@50: Chris@50: #include Chris@50: #include Chris@50: #include Chris@50: #include Chris@50: #include Chris@62: #include Chris@75: #include Chris@188: #include Chris@161: #include jtkorhonen@0: Chris@43: #include Chris@75: #include Chris@75: #include Chris@111: #include jtkorhonen@0: Chris@76: #ifndef Q_OS_WIN32 Chris@111: #include Chris@113: #include Chris@111: #include Chris@80: #endif Chris@76: Chris@172: HgRunner::HgRunner(QString myDirPath, QWidget * parent) : Chris@172: QProgressBar(parent), Chris@172: m_myDirPath(myDirPath) jtkorhonen@0: { Chris@113: m_proc = 0; Chris@84: Chris@239: // Always unbundle the extension: even if it already exists (in Chris@239: // case we're upgrading) and even if we're not going to use it (so Chris@239: // that it's available in case someone wants to use it later, Chris@239: // e.g. to fix a malfunctioning setup). But the path we actually Chris@239: // prefer is the one in the settings first, if it exists; then the Chris@239: // unbundled one; then anything in the path if for some reason Chris@239: // unbundling failed Chris@239: unbundleExtension(); Chris@239: jtkorhonen@0: setTextVisible(false); jtkorhonen@0: setVisible(false); Chris@84: m_isRunning = false; jtkorhonen@0: } jtkorhonen@0: jtkorhonen@0: HgRunner::~HgRunner() jtkorhonen@0: { Chris@111: closeTerminal(); Chris@120: if (m_proc) { Chris@120: m_proc->kill(); Chris@122: m_proc->deleteLater(); Chris@120: } jtkorhonen@0: } jtkorhonen@0: Chris@188: QString HgRunner::getUnbundledFileName() Chris@188: { Chris@240: return SettingsDialog::getUnbundledExtensionFileName(); Chris@188: } Chris@188: Chris@180: QString HgRunner::unbundleExtension() Chris@161: { Chris@188: // Pull out the bundled Python file into a temporary file, and Chris@188: // copy it to our known extension location, replacing the magic Chris@188: // text NO_EASYHG_IMPORT_PATH with our installation location Chris@188: Chris@161: QString bundled = ":easyhg.py"; Chris@188: QString unbundled = getUnbundledFileName(); Chris@188: Chris@188: QString target = QFileInfo(unbundled).path(); Chris@161: if (!QDir().mkpath(target)) { Chris@161: DEBUG << "Failed to make unbundle path " << target << endl; Chris@188: std::cerr << "Failed to make unbundle path " << target << std::endl; Chris@180: return ""; Chris@161: } Chris@188: Chris@161: QFile bf(bundled); Chris@188: DEBUG << "unbundle: bundled file will be " << bundled << endl; Chris@188: if (!bf.exists() || !bf.open(QIODevice::ReadOnly)) { Chris@180: DEBUG << "Bundled extension is missing!" << endl; Chris@180: return ""; Chris@180: } Chris@188: Chris@188: QTemporaryFile tmpfile(QString("%1/easyhg.py.XXXXXX").arg(target)); Chris@188: tmpfile.setAutoRemove(false); Chris@188: DEBUG << "unbundle: temp file will be " << tmpfile.fileName() << endl; Chris@188: if (!tmpfile.open()) { Chris@188: DEBUG << "Failed to open temporary file " << tmpfile.fileName() << endl; Chris@188: std::cerr << "Failed to open temporary file " << tmpfile.fileName() << std::endl; Chris@180: return ""; Chris@161: } Chris@188: Chris@188: QString all = QString::fromUtf8(bf.readAll()); Chris@188: all.replace("NO_EASYHG_IMPORT_PATH", m_myDirPath); Chris@188: tmpfile.write(all.toUtf8()); Chris@188: DEBUG << "unbundle: wrote " << all.length() << " characters" << endl; Chris@188: Chris@188: tmpfile.close(); Chris@188: Chris@188: QFile ef(unbundled); Chris@188: if (ef.exists()) { Chris@188: DEBUG << "unbundle: removing old file " << unbundled << endl; Chris@188: ef.remove(); Chris@188: } Chris@188: DEBUG << "unbundle: renaming " << tmpfile.fileName() << " to " << unbundled << endl; Chris@188: if (!tmpfile.rename(unbundled)) { Chris@188: DEBUG << "Failed to move temporary file to target file " << unbundled << endl; Chris@188: std::cerr << "Failed to move temporary file to target file " << unbundled << std::endl; Chris@188: return ""; Chris@188: } Chris@188: Chris@188: DEBUG << "Unbundled extension to " << unbundled << endl; Chris@188: return unbundled; Chris@161: } Chris@161: Chris@109: void HgRunner::requestAction(HgAction action) Chris@109: { Chris@109: DEBUG << "requestAction " << action.action << endl; Chris@109: bool pushIt = true; Chris@109: if (m_queue.empty()) { Chris@109: if (action == m_currentAction) { Chris@109: // this request is identical to the thing we're executing Chris@109: DEBUG << "requestAction: we're already handling this one, ignoring identical request" << endl; Chris@109: pushIt = false; Chris@109: } Chris@109: } else { Chris@109: HgAction last = m_queue.back(); Chris@109: if (action == last) { Chris@109: // this request is identical to the previous thing we Chris@109: // queued which we haven't executed yet Chris@109: DEBUG << "requestAction: we're already queueing this one, ignoring identical request" << endl; Chris@109: pushIt = false; Chris@109: } Chris@109: } Chris@109: if (pushIt) m_queue.push_back(action); Chris@109: checkQueue(); Chris@109: } Chris@109: Chris@239: QString HgRunner::getHgBinaryName() Chris@62: { Chris@62: QSettings settings; Chris@175: settings.beginGroup("Locations"); Chris@239: return settings.value("hgbinary", "").toString(); Chris@62: } Chris@62: chris@406: QString HgRunner::getSshBinaryName() chris@406: { chris@406: QSettings settings; chris@406: settings.beginGroup("Locations"); chris@406: return settings.value("sshbinary", "").toString(); chris@406: } chris@406: Chris@239: QString HgRunner::getExtensionLocation() Chris@239: { Chris@239: QSettings settings; Chris@239: settings.beginGroup("Locations"); Chris@239: QString extpath = settings.value("extensionpath", "").toString(); Chris@239: if (extpath != "" && QFile(extpath).exists()) return extpath; Chris@239: return ""; Chris@239: } Chris@239: jtkorhonen@0: void HgRunner::started() jtkorhonen@0: { Chris@104: DEBUG << "started" << endl; Chris@75: /* Chris@104: m_proc->write("blah\n"); Chris@104: m_proc->write("blah\n"); Chris@104: m_proc -> closeWriteChannel(); Chris@75: */ jtkorhonen@0: } jtkorhonen@0: Chris@84: void HgRunner::noteUsername(QString name) Chris@75: { Chris@84: m_userName = name; Chris@75: } Chris@75: Chris@84: void HgRunner::noteRealm(QString realm) Chris@75: { Chris@84: m_realm = realm; Chris@84: } Chris@84: Chris@84: void HgRunner::getUsername() Chris@84: { Chris@113: if (m_ptyFile) { Chris@84: bool ok = false; Chris@84: QString prompt = tr("User name:"); Chris@84: if (m_realm != "") { Chris@84: prompt = tr("User name for \"%1\":").arg(m_realm); Chris@84: } Chris@84: QString pwd = QInputDialog::getText Chris@84: (qobject_cast(parent()), Chris@84: tr("Enter user name"), prompt, Chris@84: QLineEdit::Normal, QString(), &ok); Chris@84: if (ok) { Chris@113: m_ptyFile->write(QString("%1\n").arg(pwd).toUtf8()); Chris@113: m_ptyFile->flush(); Chris@84: return; Chris@111: } else { Chris@111: DEBUG << "HgRunner::getUsername: user cancelled" << endl; Chris@111: killCurrentCommand(); Chris@111: return; Chris@84: } Chris@84: } Chris@84: // user cancelled or something went wrong Chris@111: DEBUG << "HgRunner::getUsername: something went wrong" << endl; Chris@84: killCurrentCommand(); Chris@84: } Chris@84: Chris@84: void HgRunner::getPassword() Chris@84: { Chris@113: if (m_ptyFile) { Chris@84: bool ok = false; Chris@84: QString prompt = tr("Password:"); Chris@84: if (m_userName != "") { Chris@84: if (m_realm != "") { Chris@84: prompt = tr("Password for \"%1\" at \"%2\":") Chris@84: .arg(m_userName).arg(m_realm); Chris@75: } else { Chris@84: prompt = tr("Password for user \"%1\":") Chris@84: .arg(m_userName); Chris@75: } Chris@75: } Chris@84: QString pwd = QInputDialog::getText Chris@84: (qobject_cast(parent()), Chris@84: tr("Enter password"), prompt, Chris@84: QLineEdit::Password, QString(), &ok); Chris@84: if (ok) { Chris@113: m_ptyFile->write(QString("%1\n").arg(pwd).toUtf8()); Chris@113: m_ptyFile->flush(); Chris@84: return; Chris@111: } else { Chris@111: DEBUG << "HgRunner::getPassword: user cancelled" << endl; Chris@111: killCurrentCommand(); Chris@111: return; Chris@84: } Chris@75: } Chris@84: // user cancelled or something went wrong Chris@111: DEBUG << "HgRunner::getPassword: something went wrong" << endl; Chris@84: killCurrentCommand(); Chris@84: } Chris@84: Chris@113: bool HgRunner::checkPrompts(QString chunk) Chris@84: { Chris@93: //DEBUG << "checkPrompts: " << chunk << endl; Chris@84: Chris@128: if (!m_currentAction.mayBeInteractive()) return false; Chris@128: Chris@84: QString text = chunk.trimmed(); Chris@84: QString lower = text.toLower(); Chris@84: if (lower.endsWith("password:")) { Chris@84: getPassword(); Chris@113: return true; Chris@84: } Chris@128: if (lower.endsWith("user:") || lower.endsWith("username:")) { Chris@84: getUsername(); Chris@113: return true; Chris@84: } Chris@128: QRegExp userRe("\\buser(name)?:\\s*([^\\s]+)"); Chris@84: if (userRe.indexIn(text) >= 0) { Chris@128: noteUsername(userRe.cap(2)); Chris@84: } Chris@84: QRegExp realmRe("\\brealmr:\\s*([^\\s]+)"); Chris@84: if (realmRe.indexIn(text) >= 0) { Chris@84: noteRealm(realmRe.cap(1)); Chris@84: } Chris@113: return false; Chris@84: } Chris@84: Chris@110: void HgRunner::dataReadyStdout() Chris@84: { Chris@110: DEBUG << "dataReadyStdout" << endl; Chris@408: if (!m_proc) return; Chris@110: QString chunk = QString::fromUtf8(m_proc->readAllStandardOutput()); Chris@113: if (!checkPrompts(chunk)) { Chris@113: m_stdout += chunk; Chris@113: } Chris@110: } Chris@110: Chris@110: void HgRunner::dataReadyStderr() Chris@110: { Chris@110: DEBUG << "dataReadyStderr" << endl; Chris@408: if (!m_proc) return; Chris@110: QString chunk = QString::fromUtf8(m_proc->readAllStandardError()); Chris@113: DEBUG << chunk; Chris@113: if (!checkPrompts(chunk)) { Chris@113: m_stderr += chunk; Chris@113: } Chris@113: } Chris@113: Chris@113: void HgRunner::dataReadyPty() Chris@113: { Chris@113: DEBUG << "dataReadyPty" << endl; Chris@113: QString chunk = QString::fromUtf8(m_ptyFile->readAll()); Chris@113: DEBUG << "chunk of " << chunk.length() << " chars" << endl; Chris@113: if (!checkPrompts(chunk)) { Chris@113: m_stdout += chunk; Chris@113: } Chris@75: } Chris@75: Chris@189: void HgRunner::error(QProcess::ProcessError) Chris@189: { Chris@189: finished(-1, QProcess::CrashExit); Chris@189: } Chris@189: jtkorhonen@0: void HgRunner::finished(int procExitCode, QProcess::ExitStatus procExitStatus) jtkorhonen@0: { chris@385: if (!m_proc) return; chris@385: chris@385: // Save the current action and reset m_currentAction before we Chris@109: // emit a signal to mark the completion; otherwise we may be Chris@109: // resetting the action after a slot has already tried to set it Chris@109: // to something else to start a new action Chris@109: Chris@109: HgAction completedAction = m_currentAction; Chris@109: Chris@84: m_isRunning = false; Chris@109: m_currentAction = HgAction(); Chris@84: Chris@195: //closeProcInput(); Chris@189: m_proc->deleteLater(); Chris@113: m_proc = 0; jtkorhonen@0: Chris@109: if (completedAction.action == ACT_NONE) { Chris@109: DEBUG << "HgRunner::finished: WARNING: completed action is ACT_NONE" << endl; Chris@62: } else { Chris@113: if (procExitCode == 0 && procExitStatus == QProcess::NormalExit) { Chris@113: DEBUG << "HgRunner::finished: Command completed successfully" Chris@113: << endl; Chris@124: // DEBUG << "stdout is " << m_stdout << endl; Chris@113: emit commandCompleted(completedAction, m_stdout); Chris@113: } else { Chris@113: DEBUG << "HgRunner::finished: Command failed, exit code " Chris@113: << procExitCode << ", exit status " << procExitStatus Chris@113: << ", stderr follows" << endl; Chris@113: DEBUG << m_stderr << endl; Chris@113: emit commandFailed(completedAction, m_stderr); Chris@113: } jtkorhonen@0: } Chris@109: Chris@109: checkQueue(); jtkorhonen@0: } jtkorhonen@0: Chris@182: void HgRunner::killCurrentActions() Chris@182: { Chris@182: m_queue.clear(); Chris@182: killCurrentCommand(); Chris@182: } Chris@182: Chris@62: void HgRunner::killCurrentCommand() jtkorhonen@0: { Chris@109: if (m_isRunning) { Chris@113: m_currentAction.action = ACT_NONE; // so that we don't bother to notify chris@385: if (m_proc) m_proc->kill(); jtkorhonen@0: } jtkorhonen@0: } jtkorhonen@0: Chris@109: void HgRunner::checkQueue() Chris@62: { Chris@109: if (m_isRunning) { Chris@109: return; Chris@109: } Chris@109: if (m_queue.empty()) { Chris@109: hide(); Chris@109: return; Chris@109: } Chris@109: HgAction toRun = m_queue.front(); Chris@109: m_queue.pop_front(); Chris@109: DEBUG << "checkQueue: have action: running " << toRun.action << endl; Chris@109: startCommand(toRun); Chris@109: } Chris@109: Chris@109: void HgRunner::startCommand(HgAction action) Chris@109: { Chris@109: QString executable = action.executable; Chris@109: bool interactive = false; Chris@109: QStringList params = action.params; Chris@109: Chris@340: if (action.workingDir.isEmpty()) { Chris@340: // We require a working directory, never just operate in pwd Chris@340: emit commandFailed(action, "EasyMercurial: No working directory supplied, will not run Mercurial command without one"); Chris@340: return; Chris@340: } Chris@340: Chris@257: QSettings settings; Chris@257: settings.beginGroup("General"); Chris@257: Chris@109: if (executable == "") { Chris@109: // This is a Hg command Chris@239: executable = getHgBinaryName(); chris@406: if (executable == "") executable = "hg"; chris@406: chris@406: QString ssh = getSshBinaryName(); chris@406: if (ssh != "") { chris@406: params.push_front(QString("ui.ssh=\"%1\"").arg(ssh)); chris@406: params.push_front("--config"); chris@406: } Chris@161: Chris@161: if (action.mayBeInteractive()) { Chris@161: params.push_front("ui.interactive=true"); Chris@161: params.push_front("--config"); Chris@176: Chris@176: if (settings.value("useextension", true).toBool()) { Chris@239: QString extpath = getExtensionLocation(); chris@407: // Looks like this one must be without quotes, even though the SSH chris@407: // one above only works on Windows if it has quotes (at least where chris@407: // there is a space in the path). Odd chris@407: params.push_front(QString("extensions.easyhg=%1").arg(extpath)); Chris@176: params.push_front("--config"); Chris@176: } Chris@161: interactive = true; Chris@161: } Chris@161: Chris@161: //!!! want an option to use the mercurial_keyring extension as well Chris@107: } jtkorhonen@0: Chris@84: m_isRunning = true; jtkorhonen@0: setRange(0, 0); Chris@115: if (!action.shouldBeFast()) show(); Chris@110: m_stdout.clear(); Chris@110: m_stderr.clear(); Chris@84: m_realm = ""; Chris@84: m_userName = ""; jtkorhonen@0: Chris@113: m_proc = new QProcess; Chris@113: Chris@177: QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); Chris@177: Chris@177: #ifdef Q_OS_WIN32 Chris@257: // On Win32 we like to bundle Hg and other executables with EasyHg Chris@177: if (m_myDirPath != "") { Chris@177: env.insert("PATH", m_myDirPath + ";" + env.value("PATH")); Chris@172: } Chris@172: #endif Chris@172: Chris@257: #ifdef Q_OS_MAC Chris@321: if (settings.value("python32", false).toBool()) { Chris@261: env.insert("VERSIONER_PYTHON_PREFER_32_BIT", "1"); Chris@257: } Chris@257: #endif Chris@257: Chris@113: env.insert("LANG", "en_US.utf8"); Chris@113: env.insert("LC_ALL", "en_US.utf8"); Chris@408: env.insert("HGENCODING", "utf8"); Chris@113: env.insert("HGPLAIN", "1"); Chris@113: m_proc->setProcessEnvironment(env); Chris@113: Chris@113: connect(m_proc, SIGNAL(started()), this, SLOT(started())); Chris@189: connect(m_proc, SIGNAL(error(QProcess::ProcessError)), Chris@189: this, SLOT(error(QProcess::ProcessError))); Chris@113: connect(m_proc, SIGNAL(finished(int, QProcess::ExitStatus)), Chris@113: this, SLOT(finished(int, QProcess::ExitStatus))); Chris@113: connect(m_proc, SIGNAL(readyReadStandardOutput()), Chris@113: this, SLOT(dataReadyStdout())); Chris@113: connect(m_proc, SIGNAL(readyReadStandardError()), Chris@113: this, SLOT(dataReadyStderr())); Chris@113: Chris@340: m_proc->setWorkingDirectory(action.workingDir); jtkorhonen@0: Chris@107: if (interactive) { Chris@111: openTerminal(); Chris@111: if (m_ptySlaveFilename != "") { Chris@113: DEBUG << "HgRunner: connecting to pseudoterminal" << endl; Chris@107: m_proc->setStandardInputFile(m_ptySlaveFilename); Chris@114: // m_proc->setStandardOutputFile(m_ptySlaveFilename); Chris@113: // m_proc->setStandardErrorFile(m_ptySlaveFilename); Chris@107: } Chris@84: } Chris@84: Chris@109: QString cmdline = executable; Chris@57: foreach (QString param, params) cmdline += " " + param; Chris@64: DEBUG << "HgRunner: starting: " << cmdline << " with cwd " Chris@109: << action.workingDir << endl; Chris@43: Chris@109: m_currentAction = action; Chris@109: Chris@113: // fill these out with what we actually ran Chris@113: m_currentAction.executable = executable; Chris@113: m_currentAction.params = params; Chris@113: Chris@109: DEBUG << "set current action to " << m_currentAction.action << endl; Chris@109: Chris@238: emit commandStarting(action); Chris@238: Chris@109: m_proc->start(executable, params); Chris@84: } Chris@84: Chris@84: void HgRunner::closeProcInput() Chris@84: { Chris@84: DEBUG << "closeProcInput" << endl; Chris@84: chris@385: if (m_proc) m_proc->closeWriteChannel(); Chris@111: } Chris@111: Chris@111: void HgRunner::openTerminal() Chris@111: { Chris@111: #ifndef Q_OS_WIN32 Chris@111: if (m_ptySlaveFilename != "") return; // already open Chris@111: DEBUG << "HgRunner::openTerminal: trying to open new pty" << endl; Chris@111: int master = posix_openpt(O_RDWR | O_NOCTTY); Chris@111: if (master < 0) { Chris@111: DEBUG << "openpt failed" << endl; Chris@111: perror("openpt failed"); Chris@111: return; Chris@111: } Chris@113: struct termios t; Chris@113: if (tcgetattr(master, &t)) { Chris@113: DEBUG << "tcgetattr failed" << endl; Chris@113: perror("tcgetattr failed"); Chris@113: } Chris@113: cfmakeraw(&t); Chris@113: if (tcsetattr(master, TCSANOW, &t)) { Chris@113: DEBUG << "tcsetattr failed" << endl; Chris@113: perror("tcsetattr failed"); Chris@113: } Chris@111: if (grantpt(master)) { Chris@111: perror("grantpt failed"); Chris@111: } Chris@111: if (unlockpt(master)) { Chris@111: perror("unlockpt failed"); Chris@111: } Chris@111: char *slave = ptsname(master); Chris@111: if (!slave) { Chris@111: perror("ptsname failed"); Chris@111: ::close(master); Chris@111: return; Chris@111: } Chris@111: m_ptyMasterFd = master; Chris@113: m_ptyFile = new QFile(); Chris@113: connect(m_ptyFile, SIGNAL(readyRead()), this, SLOT(dataReadyPty())); Chris@113: if (!m_ptyFile->open(m_ptyMasterFd, QFile::ReadWrite)) { Chris@113: DEBUG << "HgRunner::openTerminal: Failed to open QFile on master fd" << endl; Chris@113: } Chris@111: m_ptySlaveFilename = slave; Chris@111: DEBUG << "HgRunner::openTerminal: succeeded, slave is " Chris@111: << m_ptySlaveFilename << endl; Chris@111: #endif Chris@111: } Chris@111: Chris@111: void HgRunner::closeTerminal() Chris@111: { Chris@84: #ifndef Q_OS_WIN32 Chris@84: if (m_ptySlaveFilename != "") { Chris@113: delete m_ptyFile; Chris@113: m_ptyFile = 0; Chris@84: ::close(m_ptyMasterFd); Chris@84: m_ptySlaveFilename = ""; Chris@84: } Chris@84: #endif jtkorhonen@0: }