comparison 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
comparison
equal deleted inserted replaced
369:19cce6d2c470 370:b9c153e00e84
1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
2
3 /*
4 EasyMercurial
5
6 Based on HgExplorer by Jari Korhonen
7 Copyright (c) 2010 Jari Korhonen
8 Copyright (c) 2011 Chris Cannam
9 Copyright (c) 2011 Queen Mary, University of London
10
11 This program is free software; you can redistribute it and/or
12 modify it under the terms of the GNU General Public License as
13 published by the Free Software Foundation; either version 2 of the
14 License, or (at your option) any later version. See the file
15 COPYING included with this distribution for more information.
16 */
17
18 #include "hgrunner.h"
19 #include "common.h"
20 #include "debug.h"
21 #include "settingsdialog.h"
22
23 #include <QPushButton>
24 #include <QListWidget>
25 #include <QDialog>
26 #include <QLabel>
27 #include <QVBoxLayout>
28 #include <QSettings>
29 #include <QInputDialog>
30 #include <QTemporaryFile>
31 #include <QDir>
32
33 #include <iostream>
34 #include <errno.h>
35 #include <stdio.h>
36 #include <stdlib.h>
37
38 #ifndef Q_OS_WIN32
39 #include <unistd.h>
40 #include <termios.h>
41 #include <fcntl.h>
42 #endif
43
44 HgRunner::HgRunner(QString myDirPath, QWidget * parent) :
45 QProgressBar(parent),
46 m_myDirPath(myDirPath)
47 {
48 m_proc = 0;
49
50 // Always unbundle the extension: even if it already exists (in
51 // case we're upgrading) and even if we're not going to use it (so
52 // that it's available in case someone wants to use it later,
53 // e.g. to fix a malfunctioning setup). But the path we actually
54 // prefer is the one in the settings first, if it exists; then the
55 // unbundled one; then anything in the path if for some reason
56 // unbundling failed
57 unbundleExtension();
58
59 setTextVisible(false);
60 setVisible(false);
61 m_isRunning = false;
62 }
63
64 HgRunner::~HgRunner()
65 {
66 closeTerminal();
67 if (m_proc) {
68 m_proc->kill();
69 m_proc->deleteLater();
70 }
71 }
72
73 QString HgRunner::getUnbundledFileName()
74 {
75 return SettingsDialog::getUnbundledExtensionFileName();
76 }
77
78 QString HgRunner::unbundleExtension()
79 {
80 // Pull out the bundled Python file into a temporary file, and
81 // copy it to our known extension location, replacing the magic
82 // text NO_EASYHG_IMPORT_PATH with our installation location
83
84 QString bundled = ":easyhg.py";
85 QString unbundled = getUnbundledFileName();
86
87 QString target = QFileInfo(unbundled).path();
88 if (!QDir().mkpath(target)) {
89 DEBUG << "Failed to make unbundle path " << target << endl;
90 std::cerr << "Failed to make unbundle path " << target << std::endl;
91 return "";
92 }
93
94 QFile bf(bundled);
95 DEBUG << "unbundle: bundled file will be " << bundled << endl;
96 if (!bf.exists() || !bf.open(QIODevice::ReadOnly)) {
97 DEBUG << "Bundled extension is missing!" << endl;
98 return "";
99 }
100
101 QTemporaryFile tmpfile(QString("%1/easyhg.py.XXXXXX").arg(target));
102 tmpfile.setAutoRemove(false);
103 DEBUG << "unbundle: temp file will be " << tmpfile.fileName() << endl;
104 if (!tmpfile.open()) {
105 DEBUG << "Failed to open temporary file " << tmpfile.fileName() << endl;
106 std::cerr << "Failed to open temporary file " << tmpfile.fileName() << std::endl;
107 return "";
108 }
109
110 QString all = QString::fromUtf8(bf.readAll());
111 all.replace("NO_EASYHG_IMPORT_PATH", m_myDirPath);
112 tmpfile.write(all.toUtf8());
113 DEBUG << "unbundle: wrote " << all.length() << " characters" << endl;
114
115 tmpfile.close();
116
117 QFile ef(unbundled);
118 if (ef.exists()) {
119 DEBUG << "unbundle: removing old file " << unbundled << endl;
120 ef.remove();
121 }
122 DEBUG << "unbundle: renaming " << tmpfile.fileName() << " to " << unbundled << endl;
123 if (!tmpfile.rename(unbundled)) {
124 DEBUG << "Failed to move temporary file to target file " << unbundled << endl;
125 std::cerr << "Failed to move temporary file to target file " << unbundled << std::endl;
126 return "";
127 }
128
129 DEBUG << "Unbundled extension to " << unbundled << endl;
130 return unbundled;
131 }
132
133 void HgRunner::requestAction(HgAction action)
134 {
135 DEBUG << "requestAction " << action.action << endl;
136 bool pushIt = true;
137 if (m_queue.empty()) {
138 if (action == m_currentAction) {
139 // this request is identical to the thing we're executing
140 DEBUG << "requestAction: we're already handling this one, ignoring identical request" << endl;
141 pushIt = false;
142 }
143 } else {
144 HgAction last = m_queue.back();
145 if (action == last) {
146 // this request is identical to the previous thing we
147 // queued which we haven't executed yet
148 DEBUG << "requestAction: we're already queueing this one, ignoring identical request" << endl;
149 pushIt = false;
150 }
151 }
152 if (pushIt) m_queue.push_back(action);
153 checkQueue();
154 }
155
156 QString HgRunner::getHgBinaryName()
157 {
158 QSettings settings;
159 settings.beginGroup("Locations");
160 return settings.value("hgbinary", "").toString();
161 }
162
163 QString HgRunner::getExtensionLocation()
164 {
165 QSettings settings;
166 settings.beginGroup("Locations");
167 QString extpath = settings.value("extensionpath", "").toString();
168 if (extpath != "" && QFile(extpath).exists()) return extpath;
169 return "";
170 }
171
172 void HgRunner::started()
173 {
174 DEBUG << "started" << endl;
175 /*
176 m_proc->write("blah\n");
177 m_proc->write("blah\n");
178 m_proc -> closeWriteChannel();
179 */
180 }
181
182 void HgRunner::noteUsername(QString name)
183 {
184 m_userName = name;
185 }
186
187 void HgRunner::noteRealm(QString realm)
188 {
189 m_realm = realm;
190 }
191
192 void HgRunner::getUsername()
193 {
194 if (m_ptyFile) {
195 bool ok = false;
196 QString prompt = tr("User name:");
197 if (m_realm != "") {
198 prompt = tr("User name for \"%1\":").arg(m_realm);
199 }
200 QString pwd = QInputDialog::getText
201 (qobject_cast<QWidget *>(parent()),
202 tr("Enter user name"), prompt,
203 QLineEdit::Normal, QString(), &ok);
204 if (ok) {
205 m_ptyFile->write(QString("%1\n").arg(pwd).toUtf8());
206 m_ptyFile->flush();
207 return;
208 } else {
209 DEBUG << "HgRunner::getUsername: user cancelled" << endl;
210 killCurrentCommand();
211 return;
212 }
213 }
214 // user cancelled or something went wrong
215 DEBUG << "HgRunner::getUsername: something went wrong" << endl;
216 killCurrentCommand();
217 }
218
219 void HgRunner::getPassword()
220 {
221 if (m_ptyFile) {
222 bool ok = false;
223 QString prompt = tr("Password:");
224 if (m_userName != "") {
225 if (m_realm != "") {
226 prompt = tr("Password for \"%1\" at \"%2\":")
227 .arg(m_userName).arg(m_realm);
228 } else {
229 prompt = tr("Password for user \"%1\":")
230 .arg(m_userName);
231 }
232 }
233 QString pwd = QInputDialog::getText
234 (qobject_cast<QWidget *>(parent()),
235 tr("Enter password"), prompt,
236 QLineEdit::Password, QString(), &ok);
237 if (ok) {
238 m_ptyFile->write(QString("%1\n").arg(pwd).toUtf8());
239 m_ptyFile->flush();
240 return;
241 } else {
242 DEBUG << "HgRunner::getPassword: user cancelled" << endl;
243 killCurrentCommand();
244 return;
245 }
246 }
247 // user cancelled or something went wrong
248 DEBUG << "HgRunner::getPassword: something went wrong" << endl;
249 killCurrentCommand();
250 }
251
252 bool HgRunner::checkPrompts(QString chunk)
253 {
254 //DEBUG << "checkPrompts: " << chunk << endl;
255
256 if (!m_currentAction.mayBeInteractive()) return false;
257
258 QString text = chunk.trimmed();
259 QString lower = text.toLower();
260 if (lower.endsWith("password:")) {
261 getPassword();
262 return true;
263 }
264 if (lower.endsWith("user:") || lower.endsWith("username:")) {
265 getUsername();
266 return true;
267 }
268 QRegExp userRe("\\buser(name)?:\\s*([^\\s]+)");
269 if (userRe.indexIn(text) >= 0) {
270 noteUsername(userRe.cap(2));
271 }
272 QRegExp realmRe("\\brealmr:\\s*([^\\s]+)");
273 if (realmRe.indexIn(text) >= 0) {
274 noteRealm(realmRe.cap(1));
275 }
276 return false;
277 }
278
279 void HgRunner::dataReadyStdout()
280 {
281 DEBUG << "dataReadyStdout" << endl;
282 QString chunk = QString::fromUtf8(m_proc->readAllStandardOutput());
283 if (!checkPrompts(chunk)) {
284 m_stdout += chunk;
285 }
286 }
287
288 void HgRunner::dataReadyStderr()
289 {
290 DEBUG << "dataReadyStderr" << endl;
291 QString chunk = QString::fromUtf8(m_proc->readAllStandardError());
292 DEBUG << chunk;
293 if (!checkPrompts(chunk)) {
294 m_stderr += chunk;
295 }
296 }
297
298 void HgRunner::dataReadyPty()
299 {
300 DEBUG << "dataReadyPty" << endl;
301 QString chunk = QString::fromUtf8(m_ptyFile->readAll());
302 DEBUG << "chunk of " << chunk.length() << " chars" << endl;
303 if (!checkPrompts(chunk)) {
304 m_stdout += chunk;
305 }
306 }
307
308 void HgRunner::error(QProcess::ProcessError)
309 {
310 finished(-1, QProcess::CrashExit);
311 }
312
313 void HgRunner::finished(int procExitCode, QProcess::ExitStatus procExitStatus)
314 {
315 // Save the current action and reset m_currentAction before we
316 // emit a signal to mark the completion; otherwise we may be
317 // resetting the action after a slot has already tried to set it
318 // to something else to start a new action
319
320 HgAction completedAction = m_currentAction;
321
322 m_isRunning = false;
323 m_currentAction = HgAction();
324
325 //closeProcInput();
326 m_proc->deleteLater();
327 m_proc = 0;
328
329 if (completedAction.action == ACT_NONE) {
330 DEBUG << "HgRunner::finished: WARNING: completed action is ACT_NONE" << endl;
331 } else {
332 if (procExitCode == 0 && procExitStatus == QProcess::NormalExit) {
333 DEBUG << "HgRunner::finished: Command completed successfully"
334 << endl;
335 // DEBUG << "stdout is " << m_stdout << endl;
336 emit commandCompleted(completedAction, m_stdout);
337 } else {
338 DEBUG << "HgRunner::finished: Command failed, exit code "
339 << procExitCode << ", exit status " << procExitStatus
340 << ", stderr follows" << endl;
341 DEBUG << m_stderr << endl;
342 emit commandFailed(completedAction, m_stderr);
343 }
344 }
345
346 checkQueue();
347 }
348
349 void HgRunner::killCurrentActions()
350 {
351 m_queue.clear();
352 killCurrentCommand();
353 }
354
355 void HgRunner::killCurrentCommand()
356 {
357 if (m_isRunning) {
358 m_currentAction.action = ACT_NONE; // so that we don't bother to notify
359 m_proc->kill();
360 }
361 }
362
363 void HgRunner::checkQueue()
364 {
365 if (m_isRunning) {
366 return;
367 }
368 if (m_queue.empty()) {
369 hide();
370 return;
371 }
372 HgAction toRun = m_queue.front();
373 m_queue.pop_front();
374 DEBUG << "checkQueue: have action: running " << toRun.action << endl;
375 startCommand(toRun);
376 }
377
378 void HgRunner::startCommand(HgAction action)
379 {
380 QString executable = action.executable;
381 bool interactive = false;
382 QStringList params = action.params;
383
384 if (action.workingDir.isEmpty()) {
385 // We require a working directory, never just operate in pwd
386 emit commandFailed(action, "EasyMercurial: No working directory supplied, will not run Mercurial command without one");
387 return;
388 }
389
390 QSettings settings;
391 settings.beginGroup("General");
392
393 if (executable == "") {
394 // This is a Hg command
395 executable = getHgBinaryName();
396
397 if (action.mayBeInteractive()) {
398 params.push_front("ui.interactive=true");
399 params.push_front("--config");
400
401 if (settings.value("useextension", true).toBool()) {
402 QString extpath = getExtensionLocation();
403 params.push_front(QString("extensions.easyhg=%1").arg(extpath));
404 params.push_front("--config");
405 }
406 interactive = true;
407 }
408
409 //!!! want an option to use the mercurial_keyring extension as well
410 }
411
412 m_isRunning = true;
413 setRange(0, 0);
414 if (!action.shouldBeFast()) show();
415 m_stdout.clear();
416 m_stderr.clear();
417 m_realm = "";
418 m_userName = "";
419
420 m_proc = new QProcess;
421
422 QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
423
424 #ifdef Q_OS_WIN32
425 // On Win32 we like to bundle Hg and other executables with EasyHg
426 if (m_myDirPath != "") {
427 env.insert("PATH", m_myDirPath + ";" + env.value("PATH"));
428 }
429 #endif
430
431 #ifdef Q_OS_MAC
432 if (settings.value("python32", false).toBool()) {
433 env.insert("VERSIONER_PYTHON_PREFER_32_BIT", "1");
434 }
435 #endif
436
437 env.insert("LANG", "en_US.utf8");
438 env.insert("LC_ALL", "en_US.utf8");
439 env.insert("HGPLAIN", "1");
440 m_proc->setProcessEnvironment(env);
441
442 connect(m_proc, SIGNAL(started()), this, SLOT(started()));
443 connect(m_proc, SIGNAL(error(QProcess::ProcessError)),
444 this, SLOT(error(QProcess::ProcessError)));
445 connect(m_proc, SIGNAL(finished(int, QProcess::ExitStatus)),
446 this, SLOT(finished(int, QProcess::ExitStatus)));
447 connect(m_proc, SIGNAL(readyReadStandardOutput()),
448 this, SLOT(dataReadyStdout()));
449 connect(m_proc, SIGNAL(readyReadStandardError()),
450 this, SLOT(dataReadyStderr()));
451
452 m_proc->setWorkingDirectory(action.workingDir);
453
454 if (interactive) {
455 openTerminal();
456 if (m_ptySlaveFilename != "") {
457 DEBUG << "HgRunner: connecting to pseudoterminal" << endl;
458 m_proc->setStandardInputFile(m_ptySlaveFilename);
459 // m_proc->setStandardOutputFile(m_ptySlaveFilename);
460 // m_proc->setStandardErrorFile(m_ptySlaveFilename);
461 }
462 }
463
464 QString cmdline = executable;
465 foreach (QString param, params) cmdline += " " + param;
466 DEBUG << "HgRunner: starting: " << cmdline << " with cwd "
467 << action.workingDir << endl;
468
469 m_currentAction = action;
470
471 // fill these out with what we actually ran
472 m_currentAction.executable = executable;
473 m_currentAction.params = params;
474
475 DEBUG << "set current action to " << m_currentAction.action << endl;
476
477 emit commandStarting(action);
478
479 m_proc->start(executable, params);
480 }
481
482 void HgRunner::closeProcInput()
483 {
484 DEBUG << "closeProcInput" << endl;
485
486 m_proc->closeWriteChannel();
487 }
488
489 void HgRunner::openTerminal()
490 {
491 #ifndef Q_OS_WIN32
492 if (m_ptySlaveFilename != "") return; // already open
493 DEBUG << "HgRunner::openTerminal: trying to open new pty" << endl;
494 int master = posix_openpt(O_RDWR | O_NOCTTY);
495 if (master < 0) {
496 DEBUG << "openpt failed" << endl;
497 perror("openpt failed");
498 return;
499 }
500 struct termios t;
501 if (tcgetattr(master, &t)) {
502 DEBUG << "tcgetattr failed" << endl;
503 perror("tcgetattr failed");
504 }
505 cfmakeraw(&t);
506 if (tcsetattr(master, TCSANOW, &t)) {
507 DEBUG << "tcsetattr failed" << endl;
508 perror("tcsetattr failed");
509 }
510 if (grantpt(master)) {
511 perror("grantpt failed");
512 }
513 if (unlockpt(master)) {
514 perror("unlockpt failed");
515 }
516 char *slave = ptsname(master);
517 if (!slave) {
518 perror("ptsname failed");
519 ::close(master);
520 return;
521 }
522 m_ptyMasterFd = master;
523 m_ptyFile = new QFile();
524 connect(m_ptyFile, SIGNAL(readyRead()), this, SLOT(dataReadyPty()));
525 if (!m_ptyFile->open(m_ptyMasterFd, QFile::ReadWrite)) {
526 DEBUG << "HgRunner::openTerminal: Failed to open QFile on master fd" << endl;
527 }
528 m_ptySlaveFilename = slave;
529 DEBUG << "HgRunner::openTerminal: succeeded, slave is "
530 << m_ptySlaveFilename << endl;
531 #endif
532 }
533
534 void HgRunner::closeTerminal()
535 {
536 #ifndef Q_OS_WIN32
537 if (m_ptySlaveFilename != "") {
538 delete m_ptyFile;
539 m_ptyFile = 0;
540 ::close(m_ptyMasterFd);
541 m_ptySlaveFilename = "";
542 }
543 #endif
544 }