annotate hgrunner.cpp @ 161:5b2046f67a56

* Start using trivial new easyhg extension (requires PyQt)
author Chris Cannam
date Fri, 03 Dec 2010 13:36:53 +0000
parents 0ad212075b36
children 910c2c5d1873
rev   line source
Chris@57 1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
Chris@57 2
Chris@57 3 /*
Chris@57 4 EasyMercurial
Chris@57 5
Chris@57 6 Based on HgExplorer by Jari Korhonen
Chris@57 7 Copyright (c) 2010 Jari Korhonen
Chris@57 8 Copyright (c) 2010 Chris Cannam
Chris@57 9 Copyright (c) 2010 Queen Mary, University of London
Chris@57 10
Chris@57 11 This program is free software; you can redistribute it and/or
Chris@57 12 modify it under the terms of the GNU General Public License as
Chris@57 13 published by the Free Software Foundation; either version 2 of the
Chris@57 14 License, or (at your option) any later version. See the file
Chris@57 15 COPYING included with this distribution for more information.
Chris@57 16 */
jtkorhonen@0 17
jtkorhonen@0 18 #include "hgrunner.h"
Chris@62 19 #include "common.h"
Chris@57 20 #include "debug.h"
Chris@50 21
Chris@50 22 #include <QPushButton>
Chris@50 23 #include <QListWidget>
Chris@50 24 #include <QDialog>
Chris@50 25 #include <QLabel>
Chris@50 26 #include <QVBoxLayout>
Chris@62 27 #include <QSettings>
Chris@75 28 #include <QInputDialog>
Chris@161 29 #include <QDir>
jtkorhonen@0 30
Chris@43 31 #include <iostream>
Chris@75 32 #include <errno.h>
Chris@75 33 #include <stdio.h>
Chris@111 34 #include <stdlib.h>
jtkorhonen@0 35
Chris@76 36 #ifndef Q_OS_WIN32
Chris@111 37 #include <unistd.h>
Chris@113 38 #include <termios.h>
Chris@111 39 #include <fcntl.h>
Chris@80 40 #endif
Chris@76 41
jtkorhonen@0 42 HgRunner::HgRunner(QWidget * parent): QProgressBar(parent)
jtkorhonen@0 43 {
Chris@113 44 m_proc = 0;
Chris@84 45
jtkorhonen@0 46 setTextVisible(false);
jtkorhonen@0 47 setVisible(false);
Chris@84 48 m_isRunning = false;
Chris@161 49
Chris@161 50 unbundleExtension();
jtkorhonen@0 51 }
jtkorhonen@0 52
jtkorhonen@0 53 HgRunner::~HgRunner()
jtkorhonen@0 54 {
Chris@111 55 closeTerminal();
Chris@120 56 if (m_proc) {
Chris@120 57 m_proc->kill();
Chris@122 58 m_proc->deleteLater();
Chris@120 59 }
jtkorhonen@0 60 }
jtkorhonen@0 61
Chris@161 62 bool HgRunner::unbundleExtension()
Chris@161 63 {
Chris@161 64 QString bundled = ":easyhg.py";
Chris@161 65 QString home = QProcessEnvironment::systemEnvironment().value("HOME");
Chris@161 66 QString target = QString("%1/.easyhg").arg(home);
Chris@161 67 if (!QDir().mkpath(target)) {
Chris@161 68 DEBUG << "Failed to make unbundle path " << target << endl;
Chris@161 69 std::cerr << "Failed to make unbundle path " << target.toStdString() << std::endl;
Chris@161 70 return false;
Chris@161 71 }
Chris@161 72 QFile bf(bundled);
Chris@161 73 m_extensionPath = QString("%1/easyhg.py").arg(target);
Chris@161 74 if (!bf.copy(m_extensionPath)) {
Chris@161 75 DEBUG << "Failed to unbundle extension to " << target << endl;
Chris@161 76 std::cerr << "Failed to unbundle extension to " << m_extensionPath.toStdString() << std::endl;
Chris@161 77 return false;
Chris@161 78 }
Chris@161 79 DEBUG << "Unbundled extension to " << m_extensionPath << endl;
Chris@161 80 return true;
Chris@161 81 }
Chris@161 82
Chris@109 83 void HgRunner::requestAction(HgAction action)
Chris@109 84 {
Chris@109 85 DEBUG << "requestAction " << action.action << endl;
Chris@109 86 bool pushIt = true;
Chris@109 87 if (m_queue.empty()) {
Chris@109 88 if (action == m_currentAction) {
Chris@109 89 // this request is identical to the thing we're executing
Chris@109 90 DEBUG << "requestAction: we're already handling this one, ignoring identical request" << endl;
Chris@109 91 pushIt = false;
Chris@109 92 }
Chris@109 93 } else {
Chris@109 94 HgAction last = m_queue.back();
Chris@109 95 if (action == last) {
Chris@109 96 // this request is identical to the previous thing we
Chris@109 97 // queued which we haven't executed yet
Chris@109 98 DEBUG << "requestAction: we're already queueing this one, ignoring identical request" << endl;
Chris@109 99 pushIt = false;
Chris@109 100 }
Chris@109 101 }
Chris@109 102 if (pushIt) m_queue.push_back(action);
Chris@109 103 checkQueue();
Chris@109 104 }
Chris@109 105
Chris@62 106 QString HgRunner::getHgBinaryName()
Chris@62 107 {
Chris@62 108 QSettings settings;
Chris@77 109 QString hg = settings.value("hgbinary", "").toString();
Chris@77 110 if (hg == "") {
Chris@77 111 hg = findExecutable("hg");
Chris@77 112 }
Chris@77 113 if (hg != "hg") {
Chris@77 114 settings.setValue("hgbinary", hg);
Chris@77 115 }
Chris@62 116 return hg;
Chris@62 117 }
Chris@62 118
jtkorhonen@0 119 void HgRunner::started()
jtkorhonen@0 120 {
Chris@104 121 DEBUG << "started" << endl;
Chris@75 122 /*
Chris@104 123 m_proc->write("blah\n");
Chris@104 124 m_proc->write("blah\n");
Chris@104 125 m_proc -> closeWriteChannel();
Chris@75 126 */
jtkorhonen@0 127 }
jtkorhonen@0 128
Chris@84 129 void HgRunner::noteUsername(QString name)
Chris@75 130 {
Chris@84 131 m_userName = name;
Chris@75 132 }
Chris@75 133
Chris@84 134 void HgRunner::noteRealm(QString realm)
Chris@75 135 {
Chris@84 136 m_realm = realm;
Chris@84 137 }
Chris@84 138
Chris@84 139 void HgRunner::getUsername()
Chris@84 140 {
Chris@113 141 if (m_ptyFile) {
Chris@84 142 bool ok = false;
Chris@84 143 QString prompt = tr("User name:");
Chris@84 144 if (m_realm != "") {
Chris@84 145 prompt = tr("User name for \"%1\":").arg(m_realm);
Chris@84 146 }
Chris@84 147 QString pwd = QInputDialog::getText
Chris@84 148 (qobject_cast<QWidget *>(parent()),
Chris@84 149 tr("Enter user name"), prompt,
Chris@84 150 QLineEdit::Normal, QString(), &ok);
Chris@84 151 if (ok) {
Chris@113 152 m_ptyFile->write(QString("%1\n").arg(pwd).toUtf8());
Chris@113 153 m_ptyFile->flush();
Chris@84 154 return;
Chris@111 155 } else {
Chris@111 156 DEBUG << "HgRunner::getUsername: user cancelled" << endl;
Chris@111 157 killCurrentCommand();
Chris@111 158 return;
Chris@84 159 }
Chris@84 160 }
Chris@84 161 // user cancelled or something went wrong
Chris@111 162 DEBUG << "HgRunner::getUsername: something went wrong" << endl;
Chris@84 163 killCurrentCommand();
Chris@84 164 }
Chris@84 165
Chris@84 166 void HgRunner::getPassword()
Chris@84 167 {
Chris@113 168 if (m_ptyFile) {
Chris@84 169 bool ok = false;
Chris@84 170 QString prompt = tr("Password:");
Chris@84 171 if (m_userName != "") {
Chris@84 172 if (m_realm != "") {
Chris@84 173 prompt = tr("Password for \"%1\" at \"%2\":")
Chris@84 174 .arg(m_userName).arg(m_realm);
Chris@75 175 } else {
Chris@84 176 prompt = tr("Password for user \"%1\":")
Chris@84 177 .arg(m_userName);
Chris@75 178 }
Chris@75 179 }
Chris@84 180 QString pwd = QInputDialog::getText
Chris@84 181 (qobject_cast<QWidget *>(parent()),
Chris@84 182 tr("Enter password"), prompt,
Chris@84 183 QLineEdit::Password, QString(), &ok);
Chris@84 184 if (ok) {
Chris@113 185 m_ptyFile->write(QString("%1\n").arg(pwd).toUtf8());
Chris@113 186 m_ptyFile->flush();
Chris@84 187 return;
Chris@111 188 } else {
Chris@111 189 DEBUG << "HgRunner::getPassword: user cancelled" << endl;
Chris@111 190 killCurrentCommand();
Chris@111 191 return;
Chris@84 192 }
Chris@75 193 }
Chris@84 194 // user cancelled or something went wrong
Chris@111 195 DEBUG << "HgRunner::getPassword: something went wrong" << endl;
Chris@84 196 killCurrentCommand();
Chris@84 197 }
Chris@84 198
Chris@113 199 bool HgRunner::checkPrompts(QString chunk)
Chris@84 200 {
Chris@93 201 //DEBUG << "checkPrompts: " << chunk << endl;
Chris@84 202
Chris@128 203 if (!m_currentAction.mayBeInteractive()) return false;
Chris@128 204
Chris@84 205 QString text = chunk.trimmed();
Chris@84 206 QString lower = text.toLower();
Chris@84 207 if (lower.endsWith("password:")) {
Chris@84 208 getPassword();
Chris@113 209 return true;
Chris@84 210 }
Chris@128 211 if (lower.endsWith("user:") || lower.endsWith("username:")) {
Chris@84 212 getUsername();
Chris@113 213 return true;
Chris@84 214 }
Chris@128 215 QRegExp userRe("\\buser(name)?:\\s*([^\\s]+)");
Chris@84 216 if (userRe.indexIn(text) >= 0) {
Chris@128 217 noteUsername(userRe.cap(2));
Chris@84 218 }
Chris@84 219 QRegExp realmRe("\\brealmr:\\s*([^\\s]+)");
Chris@84 220 if (realmRe.indexIn(text) >= 0) {
Chris@84 221 noteRealm(realmRe.cap(1));
Chris@84 222 }
Chris@113 223 return false;
Chris@84 224 }
Chris@84 225
Chris@110 226 void HgRunner::dataReadyStdout()
Chris@84 227 {
Chris@110 228 DEBUG << "dataReadyStdout" << endl;
Chris@110 229 QString chunk = QString::fromUtf8(m_proc->readAllStandardOutput());
Chris@113 230 if (!checkPrompts(chunk)) {
Chris@113 231 m_stdout += chunk;
Chris@113 232 }
Chris@110 233 }
Chris@110 234
Chris@110 235 void HgRunner::dataReadyStderr()
Chris@110 236 {
Chris@110 237 DEBUG << "dataReadyStderr" << endl;
Chris@110 238 QString chunk = QString::fromUtf8(m_proc->readAllStandardError());
Chris@113 239 DEBUG << chunk;
Chris@113 240 if (!checkPrompts(chunk)) {
Chris@113 241 m_stderr += chunk;
Chris@113 242 }
Chris@113 243 }
Chris@113 244
Chris@113 245 void HgRunner::dataReadyPty()
Chris@113 246 {
Chris@113 247 DEBUG << "dataReadyPty" << endl;
Chris@113 248 QString chunk = QString::fromUtf8(m_ptyFile->readAll());
Chris@113 249 DEBUG << "chunk of " << chunk.length() << " chars" << endl;
Chris@113 250 if (!checkPrompts(chunk)) {
Chris@113 251 m_stdout += chunk;
Chris@113 252 }
Chris@75 253 }
Chris@75 254
jtkorhonen@0 255 void HgRunner::finished(int procExitCode, QProcess::ExitStatus procExitStatus)
jtkorhonen@0 256 {
Chris@109 257 // Save the current action and reset m_currentAction before we
Chris@109 258 // emit a signal to mark the completion; otherwise we may be
Chris@109 259 // resetting the action after a slot has already tried to set it
Chris@109 260 // to something else to start a new action
Chris@109 261
Chris@109 262 HgAction completedAction = m_currentAction;
Chris@109 263
Chris@84 264 m_isRunning = false;
Chris@109 265 m_currentAction = HgAction();
Chris@84 266
Chris@84 267 closeProcInput();
Chris@113 268 delete m_proc;
Chris@113 269 m_proc = 0;
jtkorhonen@0 270
Chris@109 271 if (completedAction.action == ACT_NONE) {
Chris@109 272 DEBUG << "HgRunner::finished: WARNING: completed action is ACT_NONE" << endl;
Chris@62 273 } else {
Chris@113 274 if (procExitCode == 0 && procExitStatus == QProcess::NormalExit) {
Chris@113 275 DEBUG << "HgRunner::finished: Command completed successfully"
Chris@113 276 << endl;
Chris@124 277 // DEBUG << "stdout is " << m_stdout << endl;
Chris@113 278 emit commandCompleted(completedAction, m_stdout);
Chris@113 279 } else {
Chris@113 280 DEBUG << "HgRunner::finished: Command failed, exit code "
Chris@113 281 << procExitCode << ", exit status " << procExitStatus
Chris@113 282 << ", stderr follows" << endl;
Chris@113 283 DEBUG << m_stderr << endl;
Chris@113 284 emit commandFailed(completedAction, m_stderr);
Chris@113 285 }
jtkorhonen@0 286 }
Chris@109 287
Chris@109 288 checkQueue();
jtkorhonen@0 289 }
jtkorhonen@0 290
Chris@62 291 void HgRunner::killCurrentCommand()
jtkorhonen@0 292 {
Chris@109 293 if (m_isRunning) {
Chris@113 294 m_currentAction.action = ACT_NONE; // so that we don't bother to notify
Chris@113 295 m_proc->kill();
jtkorhonen@0 296 }
jtkorhonen@0 297 }
jtkorhonen@0 298
Chris@109 299 void HgRunner::checkQueue()
Chris@62 300 {
Chris@109 301 if (m_isRunning) {
Chris@109 302 return;
Chris@109 303 }
Chris@109 304 if (m_queue.empty()) {
Chris@109 305 hide();
Chris@109 306 return;
Chris@109 307 }
Chris@109 308 HgAction toRun = m_queue.front();
Chris@109 309 m_queue.pop_front();
Chris@109 310 DEBUG << "checkQueue: have action: running " << toRun.action << endl;
Chris@109 311 startCommand(toRun);
Chris@109 312 }
Chris@109 313
Chris@109 314 void HgRunner::startCommand(HgAction action)
Chris@109 315 {
Chris@109 316 QString executable = action.executable;
Chris@109 317 bool interactive = false;
Chris@109 318 QStringList params = action.params;
Chris@109 319
Chris@109 320 if (executable == "") {
Chris@109 321 // This is a Hg command
Chris@109 322 executable = getHgBinaryName();
Chris@161 323
Chris@161 324 if (action.mayBeInteractive()) {
Chris@161 325 params.push_front("ui.interactive=true");
Chris@161 326 params.push_front("--config");
Chris@161 327 params.push_front(QString("extensions.easyhg=%1").arg(m_extensionPath));
Chris@161 328 params.push_front("--config");
Chris@161 329 interactive = true;
Chris@161 330 }
Chris@161 331
Chris@161 332 //!!! want an option to use the mercurial_keyring extension as well
Chris@161 333
Chris@161 334 /*
Chris@104 335 #ifdef Q_OS_WIN32
Chris@109 336 // This at least means we won't block on the non-working password prompt
Chris@109 337 params.push_front("--noninteractive");
Chris@104 338 #else
Chris@109 339 // password prompt should work here
Chris@109 340 if (action.mayBeInteractive()) {
Chris@109 341 params.push_front("ui.interactive=true");
Chris@109 342 params.push_front("--config");
Chris@109 343 interactive = true;
Chris@109 344 } else {
Chris@109 345 params.push_front("--noninteractive");
Chris@109 346 }
Chris@144 347 #endif
Chris@161 348 */
Chris@107 349 }
jtkorhonen@0 350
Chris@84 351 m_isRunning = true;
jtkorhonen@0 352 setRange(0, 0);
Chris@115 353 if (!action.shouldBeFast()) show();
Chris@110 354 m_stdout.clear();
Chris@110 355 m_stderr.clear();
Chris@84 356 m_realm = "";
Chris@84 357 m_userName = "";
jtkorhonen@0 358
Chris@113 359 m_proc = new QProcess;
Chris@113 360
Chris@113 361 QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
Chris@113 362 env.insert("LANG", "en_US.utf8");
Chris@113 363 env.insert("LC_ALL", "en_US.utf8");
Chris@113 364 env.insert("HGPLAIN", "1");
Chris@113 365 m_proc->setProcessEnvironment(env);
Chris@113 366
Chris@113 367 connect(m_proc, SIGNAL(started()), this, SLOT(started()));
Chris@113 368 connect(m_proc, SIGNAL(finished(int, QProcess::ExitStatus)),
Chris@113 369 this, SLOT(finished(int, QProcess::ExitStatus)));
Chris@113 370 connect(m_proc, SIGNAL(readyReadStandardOutput()),
Chris@113 371 this, SLOT(dataReadyStdout()));
Chris@113 372 connect(m_proc, SIGNAL(readyReadStandardError()),
Chris@113 373 this, SLOT(dataReadyStderr()));
Chris@113 374
Chris@109 375 if (!action.workingDir.isEmpty()) {
Chris@109 376 m_proc->setWorkingDirectory(action.workingDir);
jtkorhonen@0 377 }
jtkorhonen@0 378
Chris@107 379 if (interactive) {
Chris@111 380 openTerminal();
Chris@111 381 if (m_ptySlaveFilename != "") {
Chris@113 382 DEBUG << "HgRunner: connecting to pseudoterminal" << endl;
Chris@107 383 m_proc->setStandardInputFile(m_ptySlaveFilename);
Chris@114 384 // m_proc->setStandardOutputFile(m_ptySlaveFilename);
Chris@113 385 // m_proc->setStandardErrorFile(m_ptySlaveFilename);
Chris@107 386 }
Chris@84 387 }
Chris@84 388
Chris@109 389 QString cmdline = executable;
Chris@57 390 foreach (QString param, params) cmdline += " " + param;
Chris@64 391 DEBUG << "HgRunner: starting: " << cmdline << " with cwd "
Chris@109 392 << action.workingDir << endl;
Chris@43 393
Chris@109 394 m_currentAction = action;
Chris@109 395
Chris@113 396 // fill these out with what we actually ran
Chris@113 397 m_currentAction.executable = executable;
Chris@113 398 m_currentAction.params = params;
Chris@113 399
Chris@109 400 DEBUG << "set current action to " << m_currentAction.action << endl;
Chris@109 401
Chris@109 402 m_proc->start(executable, params);
Chris@84 403 }
Chris@84 404
Chris@84 405 void HgRunner::closeProcInput()
Chris@84 406 {
Chris@84 407 DEBUG << "closeProcInput" << endl;
Chris@84 408
Chris@84 409 m_proc->closeWriteChannel();
Chris@111 410 }
Chris@111 411
Chris@111 412 void HgRunner::openTerminal()
Chris@111 413 {
Chris@111 414 #ifndef Q_OS_WIN32
Chris@111 415 if (m_ptySlaveFilename != "") return; // already open
Chris@111 416 DEBUG << "HgRunner::openTerminal: trying to open new pty" << endl;
Chris@111 417 int master = posix_openpt(O_RDWR | O_NOCTTY);
Chris@111 418 if (master < 0) {
Chris@111 419 DEBUG << "openpt failed" << endl;
Chris@111 420 perror("openpt failed");
Chris@111 421 return;
Chris@111 422 }
Chris@113 423 struct termios t;
Chris@113 424 if (tcgetattr(master, &t)) {
Chris@113 425 DEBUG << "tcgetattr failed" << endl;
Chris@113 426 perror("tcgetattr failed");
Chris@113 427 }
Chris@113 428 cfmakeraw(&t);
Chris@113 429 if (tcsetattr(master, TCSANOW, &t)) {
Chris@113 430 DEBUG << "tcsetattr failed" << endl;
Chris@113 431 perror("tcsetattr failed");
Chris@113 432 }
Chris@111 433 if (grantpt(master)) {
Chris@111 434 perror("grantpt failed");
Chris@111 435 }
Chris@111 436 if (unlockpt(master)) {
Chris@111 437 perror("unlockpt failed");
Chris@111 438 }
Chris@111 439 char *slave = ptsname(master);
Chris@111 440 if (!slave) {
Chris@111 441 perror("ptsname failed");
Chris@111 442 ::close(master);
Chris@111 443 return;
Chris@111 444 }
Chris@111 445 m_ptyMasterFd = master;
Chris@113 446 m_ptyFile = new QFile();
Chris@113 447 connect(m_ptyFile, SIGNAL(readyRead()), this, SLOT(dataReadyPty()));
Chris@113 448 if (!m_ptyFile->open(m_ptyMasterFd, QFile::ReadWrite)) {
Chris@113 449 DEBUG << "HgRunner::openTerminal: Failed to open QFile on master fd" << endl;
Chris@113 450 }
Chris@111 451 m_ptySlaveFilename = slave;
Chris@111 452 DEBUG << "HgRunner::openTerminal: succeeded, slave is "
Chris@111 453 << m_ptySlaveFilename << endl;
Chris@111 454 #endif
Chris@111 455 }
Chris@111 456
Chris@111 457 void HgRunner::closeTerminal()
Chris@111 458 {
Chris@84 459 #ifndef Q_OS_WIN32
Chris@84 460 if (m_ptySlaveFilename != "") {
Chris@113 461 delete m_ptyFile;
Chris@113 462 m_ptyFile = 0;
Chris@84 463 ::close(m_ptyMasterFd);
Chris@84 464 m_ptySlaveFilename = "";
Chris@84 465 }
Chris@84 466 #endif
jtkorhonen@0 467 }