Mercurial > hg > easyhg
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 } |