Switch use of threads on defined(CAVERNLOG_USE_THREADS)
[survex.git] / src / cavernlog.cc
blobf9eb92280df696ac5919e5a31b8d2578b64b5239
1 /* cavernlog.cc
2 * Run cavern inside an Aven window
4 * Copyright (C) 2005,2006,2010,2011,2012,2014,2015,2016 Olly Betts
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
21 #ifdef HAVE_CONFIG_H
22 # include <config.h>
23 #endif
25 #include "aven.h"
26 #include "cavernlog.h"
27 #include "filename.h"
28 #include "mainfrm.h"
29 #include "message.h"
31 #include <errno.h>
32 #include <stdio.h>
33 #include <stdlib.h>
35 // For select():
36 #ifdef HAVE_SYS_SELECT_H
37 #include <sys/select.h>
38 #endif
39 #include <sys/time.h>
40 #include <sys/types.h>
41 #include <unistd.h>
43 enum { LOG_REPROCESS = 1234, LOG_SAVE = 1235 };
45 #ifdef CAVERNLOG_USE_THREADS
46 // New event type for passing a chunk of cavern output from the worker thread
47 // to the main thread.
48 class CavernOutputEvent;
50 wxDEFINE_EVENT(wxEVT_CAVERN_OUTPUT, CavernOutputEvent);
52 class CavernOutputEvent : public wxEvent {
53 public:
54 char buf[1000];
55 int len;
56 CavernOutputEvent() : wxEvent(0, wxEVT_CAVERN_OUTPUT), len(0) { }
58 wxEvent * Clone() const {
59 CavernOutputEvent * e = new CavernOutputEvent();
60 e->len = len;
61 if (len > 0) memcpy(e->buf, buf, len);
62 return e;
66 class CavernThread : public wxThread {
67 protected:
68 virtual ExitCode Entry();
70 CavernLogWindow *handler;
72 int cavern_fd;
74 public:
75 CavernThread(CavernLogWindow *handler_, int fd)
76 : wxThread(wxTHREAD_DETACHED), handler(handler_), cavern_fd(fd) { }
78 ~CavernThread() {
79 wxCriticalSectionLocker enter(handler->thread_lock);
80 handler->thread = NULL;
84 wxThread::ExitCode
85 CavernThread::Entry()
87 ssize_t n;
88 do {
89 CavernOutputEvent * e = new CavernOutputEvent();
90 e->len = n = read(cavern_fd, e->buf, sizeof(e->buf));
91 if (TestDestroy()) {
92 delete e;
93 break;
95 wxQueueEvent(handler, e);
96 } while (n > 0);
97 return (wxThread::ExitCode)0;
99 #endif
101 BEGIN_EVENT_TABLE(CavernLogWindow, wxHtmlWindow)
102 EVT_BUTTON(LOG_REPROCESS, CavernLogWindow::OnReprocess)
103 EVT_BUTTON(LOG_SAVE, CavernLogWindow::OnSave)
104 EVT_BUTTON(wxID_OK, CavernLogWindow::OnOK)
105 #ifdef CAVERNLOG_USE_THREADS
106 EVT_CLOSE(CavernLogWindow::OnClose)
107 EVT_COMMAND(wxID_ANY, wxEVT_CAVERN_OUTPUT, CavernLogWindow::OnCavernOutput)
108 #else
109 EVT_IDLE(CavernLogWindow::OnIdle)
110 #endif
111 END_EVENT_TABLE()
113 static wxString escape_for_shell(wxString s, bool protect_dash = false)
115 size_t p = 0;
116 #ifdef __WXMSW__
117 // Correct quoting here is insane - you need to quote for CommandLineToArgV
118 // and then also for cmd.exe:
119 // http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx
120 bool needs_quotes = false;
121 while (p < s.size()) {
122 wxChar ch = s[p];
123 if (ch < 127) {
124 if (ch == wxT('"')) {
125 s.insert(p, wxT("\\^"));
126 p += 2;
127 needs_quotes = true;
128 } else if (ch == ' ') {
129 needs_quotes = true;
130 } else if (strchr("()%<>&|^", ch)) {
131 s.insert(p, wxT('^'));
132 ++p;
135 ++p;
137 if (needs_quotes) {
138 s.insert(0u, wxT("^\""));
139 s += wxT("^\"");
141 #else
142 if (protect_dash && !s.empty() && s[0u] == '-') {
143 // If the filename starts with a '-', protect it from being
144 // treated as an option by prepending "./".
145 s.insert(0, wxT("./"));
146 p = 2;
148 while (p < s.size()) {
149 // Exclude a few safe characters which are common in filenames
150 if (!isalnum(s[p]) && strchr("/._-", s[p]) == NULL) {
151 s.insert(p, 1, wxT('\\'));
152 ++p;
154 ++p;
156 #endif
157 return s;
160 CavernLogWindow::CavernLogWindow(MainFrm * mainfrm_, const wxString & survey_, wxWindow * parent)
161 : wxHtmlWindow(parent), mainfrm(mainfrm_), cavern_out(NULL),
162 link_count(0), end(buf), init_done(false), survey(survey_)
163 #ifdef CAVERNLOG_USE_THREADS
164 , thread(NULL)
165 #endif
167 int fsize = parent->GetFont().GetPointSize();
168 int sizes[7] = { fsize, fsize, fsize, fsize, fsize, fsize, fsize };
169 SetFonts(wxString(), wxString(), sizes);
172 CavernLogWindow::~CavernLogWindow()
174 if (cavern_out) {
175 wxEndBusyCursor();
176 pclose(cavern_out);
180 #ifdef CAVERNLOG_USE_THREADS
181 void
182 CavernLogWindow::stop_thread()
185 wxCriticalSectionLocker enter(thread_lock);
186 if (thread) {
187 wxThreadError res;
188 #if wxCHECK_VERSION(2,9,2)
189 res = thread->Delete(NULL, wxTHREAD_WAIT_BLOCK);
190 #else
191 res = thread->Delete();
192 #endif
193 if (res != wxTHREAD_NO_ERROR) {
194 // FIXME
199 // Wait for thread to complete.
200 while (true) {
202 wxCriticalSectionLocker enter(thread_lock);
203 if (!thread) break;
205 wxMilliSleep(1);
209 void
210 CavernLogWindow::OnClose(wxCloseEvent &)
212 if (thread) stop_thread();
213 Destroy();
215 #endif
217 void
218 CavernLogWindow::OnLinkClicked(const wxHtmlLinkInfo &link)
220 wxString href = link.GetHref();
221 wxString title = link.GetTarget();
222 size_t colon2 = href.rfind(wxT(':'));
223 if (colon2 == wxString::npos)
224 return;
225 size_t colon = href.rfind(wxT(':'), colon2 - 1);
226 if (colon == wxString::npos)
227 return;
228 #ifdef __WXMSW__
229 wxString cmd = wxT("notepad $f");
230 #elif defined __WXMAC__
231 wxString cmd = wxT("open -t $f");
232 #else
233 wxString cmd = wxT("x-terminal-emulator -title $t -e vim +'call cursor($l,$c)' $f");
234 // wxString cmd = wxT("gedit -b $f +$l:$c $f");
235 // wxString cmd = wxT("x-terminal-emulator -title $t -e emacs +$l $f");
236 // wxString cmd = wxT("x-terminal-emulator -title $t -e nano +$l $f");
237 // wxString cmd = wxT("x-terminal-emulator -title $t -e jed -g $l $f");
238 #endif
239 wxChar * p = wxGetenv(wxT("SURVEXEDITOR"));
240 if (p) {
241 cmd = p;
242 if (!cmd.find(wxT("$f"))) {
243 cmd += wxT(" $f");
246 size_t i = 0;
247 while ((i = cmd.find(wxT('$'), i)) != wxString::npos) {
248 if (++i >= cmd.size()) break;
249 switch ((int)cmd[i]) {
250 case wxT('$'):
251 cmd.erase(i, 1);
252 break;
253 case wxT('f'): {
254 wxString f = escape_for_shell(href.substr(0, colon), true);
255 cmd.replace(i - 1, 2, f);
256 i += f.size() - 1;
257 break;
259 case wxT('t'): {
260 wxString t = escape_for_shell(title);
261 cmd.replace(i - 1, 2, t);
262 i += t.size() - 1;
263 break;
265 case wxT('l'): {
266 wxString l = escape_for_shell(href.substr(colon + 1, colon2 - colon - 1));
267 cmd.replace(i - 1, 2, l);
268 i += l.size() - 1;
269 break;
271 case wxT('c'): {
272 wxString l;
273 if (colon2 >= href.size())
274 l = wxT("0");
275 else
276 l = escape_for_shell(href.substr(colon2 + 1));
277 cmd.replace(i - 1, 2, l);
278 i += l.size() - 1;
279 break;
281 default:
282 ++i;
285 if (wxSystem(cmd) >= 0)
286 return;
287 wxString m;
288 // TRANSLATORS: %s is replaced by the command we attempted to run.
289 m.Printf(wmsg(/*Couldn’t run external command: “%s”*/17), cmd.c_str());
290 m += wxT(" (");
291 m += wxString(strerror(errno), wxConvUTF8);
292 m += wxT(')');
293 wxGetApp().ReportError(m);
296 void
297 CavernLogWindow::process(const wxString &file)
299 SetPage(wxString());
300 #ifdef CAVERNLOG_USE_THREADS
301 if (thread) stop_thread();
302 #endif
303 if (cavern_out) {
304 pclose(cavern_out);
305 cavern_out = NULL;
306 } else {
307 wxBeginBusyCursor();
310 SetFocus();
311 filename = file;
313 link_count = 0;
314 cur.resize(0);
315 log_txt.resize(0);
317 #ifdef __WXMSW__
318 SetEnvironmentVariable(wxT("SURVEX_UTF8"), wxT("1"));
319 #else
320 setenv("SURVEX_UTF8", "1", 1);
321 #endif
323 wxString escaped_file = escape_for_shell(file, true);
324 #ifdef __WXMSW__
325 wxString cmd;
327 DWORD len = 256;
328 wchar_t *buf = NULL;
329 while (1) {
330 DWORD got;
331 buf = (wchar_t*)osrealloc(buf, len * 2);
332 got = GetModuleFileNameW(NULL, buf, len);
333 if (got < len) break;
334 len += len;
336 /* Strange Win32 nastiness - strip prefix "\\?\" if present */
337 if (wcsncmp(buf, L"\\\\?\\", 4) == 0) buf += 4;
338 wchar_t * slash = wcsrchr(buf, L'\\');
339 if (slash) {
340 cmd.assign(buf, slash - buf + 1);
343 cmd += L"cavern";
344 cmd = escape_for_shell(cmd, false);
345 #else
346 char *cavern = use_path(msg_exepth(), "cavern");
347 wxString cmd = escape_for_shell(wxString(cavern, wxConvUTF8), false);
348 osfree(cavern);
349 #endif
350 cmd += wxT(" -o ");
351 cmd += escaped_file;
352 cmd += wxT(' ');
353 cmd += escaped_file;
355 #ifdef __WXMSW__
356 cavern_out = _wpopen(cmd.c_str(), L"r");
357 #else
358 cavern_out = popen(cmd.mb_str(), "r");
359 #endif
360 if (!cavern_out) {
361 wxString m;
362 m.Printf(wmsg(/*Couldn’t run external command: “%s”*/17), cmd.c_str());
363 m += wxT(" (");
364 m += wxString(strerror(errno), wxConvUTF8);
365 m += wxT(')');
366 wxGetApp().ReportError(m);
367 return;
369 #ifndef CAVERNLOG_USE_THREADS
372 void
373 CavernLogWindow::OnIdle(wxIdleEvent& event)
375 if (cavern_out == NULL) return;
376 #endif
378 int cavern_fd;
379 #ifdef __WXMSW__
380 cavern_fd = _fileno(cavern_out);
381 #else
382 cavern_fd = fileno(cavern_out);
383 #endif
384 #ifdef CAVERNLOG_USE_THREADS
385 thread = new CavernThread(this, cavern_fd);
386 if (thread->Run() != wxTHREAD_NO_ERROR) {
387 wxGetApp().ReportError(wxT("Thread failed to start"));
388 delete thread;
389 thread = NULL;
393 void
394 CavernLogWindow::OnCavernOutput(wxCommandEvent & e_)
396 CavernOutputEvent & e = (CavernOutputEvent&)e_;
398 if (e.len > 0) {
399 ssize_t n = e.len;
400 if (size_t(n) > sizeof(buf) - (end - buf)) abort();
401 memcpy(end, e.buf, n);
402 #else
403 ssize_t n;
404 #ifndef __WXMSW__
405 assert(cavern_fd < FD_SETSIZE); // FIXME we shouldn't just assert, but what else to do?
407 fd_set rfds, efds;
408 FD_ZERO(&rfds);
409 FD_ZERO(&efds);
410 FD_SET(cavern_fd, &rfds);
411 FD_SET(cavern_fd, &efds);
412 // Wait up to 0.01 seconds for data to avoid a tight idle loop.
413 struct timeval timeout;
414 timeout.tv_sec = 0;
415 timeout.tv_usec = 10000;
416 int r = select(cavern_fd + 1, &rfds, NULL, &efds, &timeout);
417 if (r == 0) {
418 // No new output to process.
419 event.RequestMore();
420 return;
422 if (r <= 0 || !FD_ISSET(cavern_fd, &rfds))
423 goto abort;
424 #endif
425 n = read(cavern_fd, end, sizeof(buf) - (end - buf));
426 if (n > 0) {
427 #endif
428 log_txt.append((const char *)end, n);
429 end += n;
431 const unsigned char * p = buf;
433 while (p != end) {
434 int ch = *p++;
435 if (ch >= 0x80) {
436 // Decode multi-byte UTF-8 sequence.
437 if (ch < 0xc0) {
438 // Invalid UTF-8 sequence.
439 goto bad_utf8;
440 } else if (ch < 0xe0) {
441 /* 2 byte sequence */
442 if (p == end) {
443 // Incomplete UTF-8 sequence - try to read more.
444 break;
446 int ch1 = *p++;
447 if ((ch1 & 0xc0) != 0x80) {
448 // Invalid UTF-8 sequence.
449 goto bad_utf8;
451 ch = ((ch & 0x1f) << 6) | (ch1 & 0x3f);
452 } else if (ch < 0xf0) {
453 /* 3 byte sequence */
454 if (end - p <= 1) {
455 // Incomplete UTF-8 sequence - try to read more.
456 break;
458 int ch1 = *p++;
459 ch = ((ch & 0x1f) << 12) | ((ch1 & 0x3f) << 6);
460 if ((ch1 & 0xc0) != 0x80) {
461 // Invalid UTF-8 sequence.
462 goto bad_utf8;
464 int ch2 = *p++;
465 if ((ch2 & 0xc0) != 0x80) {
466 // Invalid UTF-8 sequence.
467 goto bad_utf8;
469 ch |= (ch2 & 0x3f);
470 } else {
471 // Overlong UTF-8 sequence.
472 goto bad_utf8;
476 switch (ch) {
477 case '\r':
478 // Ignore.
479 break;
480 case '\n': {
481 if (cur.empty()) continue;
482 #ifndef __WXMSW__
483 size_t colon = cur.find(':');
484 #else
485 // If the path is "C:\path\to\file.svx" then don't split at the
486 // : after the drive letter! FIXME: better to look for ": "?
487 size_t colon = cur.find(':', 2);
488 #endif
489 if (colon != wxString::npos && colon < cur.size() - 2) {
490 ++colon;
491 size_t i = colon;
492 while (i < cur.size() - 2 &&
493 cur[i] >= wxT('0') && cur[i] <= wxT('9')) {
494 ++i;
496 if (i > colon && cur[i] == wxT(':') ) {
497 colon = i;
498 // Check for column number.
499 while (++i < cur.size() - 2 &&
500 cur[i] >= wxT('0') && cur[i] <= wxT('9')) { }
501 if (i > colon + 1 && cur[i] == wxT(':') ) {
502 colon = i;
503 } else {
504 // If there's no colon, include a trailing ':'
505 // so that we can unambiguously split the href
506 // value up into filename, line and column.
507 ++colon;
509 wxString tag = wxT("<a href=\"");
510 tag.append(cur, 0, colon);
511 while (cur[++i] == wxT(' ')) { }
512 tag += wxT("\" target=\"");
513 tag.append(cur, i, wxString::npos);
514 tag += wxT("\">");
515 cur.insert(0, tag);
516 size_t offset = colon + tag.size();
517 cur.insert(offset, wxT("</a>"));
518 offset += 4 + 2;
520 static const wxString & error_marker = wmsg(/*error*/93) + ":";
521 static const wxString & warning_marker = wmsg(/*warning*/4) + ":";
523 if (cur.substr(offset, error_marker.size()) == error_marker) {
524 // Show "error" marker in red.
525 cur.insert(offset, wxT("<span style=\"color:red\">"));
526 offset += 24 + error_marker.size() - 1;
527 cur.insert(offset, wxT("</span>"));
528 } else if (cur.substr(offset, warning_marker.size()) == warning_marker) {
529 // Show "warning" marker in orange.
530 cur.insert(offset, wxT("<span style=\"color:orange\">"));
531 offset += 27 + warning_marker.size() - 1;
532 cur.insert(offset, wxT("</span>"));
535 ++link_count;
539 // Save the scrollbar positions.
540 int scroll_x = 0, scroll_y = 0;
541 GetViewStart(&scroll_x, &scroll_y);
543 cur += wxT("<br>\n");
544 AppendToPage(cur);
546 if (!link_count) {
547 // Auto-scroll the window until we've reported a
548 // warning or error.
549 int x, y;
550 GetVirtualSize(&x, &y);
551 int xs, ys;
552 GetClientSize(&xs, &ys);
553 y -= ys;
554 int xu, yu;
555 GetScrollPixelsPerUnit(&xu, &yu);
556 Scroll(scroll_x, y / yu);
557 } else {
558 // Restore the scrollbar positions.
559 Scroll(scroll_x, scroll_y);
562 cur.clear();
563 break;
565 case '<':
566 cur += wxT("&lt;");
567 break;
568 case '>':
569 cur += wxT("&gt;");
570 break;
571 case '&':
572 cur += wxT("&amp;");
573 break;
574 case '"':
575 cur += wxT("&#22;");
576 continue;
577 default:
578 if (ch >= 128) {
579 cur += wxString::Format(wxT("&#%u;"), ch);
580 } else {
581 cur += (char)ch;
586 size_t left = end - p;
587 end = buf + left;
588 if (left) memmove(buf, p, left);
589 Update();
590 #ifndef CAVERNLOG_USE_THREADS
591 event.RequestMore();
592 #endif
593 return;
596 #ifdef CAVERNLOG_USE_THREADS
597 if (e.len == 0 && buf != end) {
598 // Truncated UTF-8 sequence.
599 goto bad_utf8;
601 #else
602 if (n == 0 && buf != end) {
603 // Truncated UTF-8 sequence.
604 goto bad_utf8;
606 #endif
608 if (false) {
609 bad_utf8:
610 errno = EILSEQ;
612 #if !defined CAVERNLOG_USE_THREADS && !defined __WXMSW__
613 abort:
614 #endif
616 /* TRANSLATORS: Label for button in aven’s cavern log window which
617 * allows the user to save the log to a file. */
618 AppendToPage(wxString::Format(wxT("<avenbutton id=%d name=\"%s\">"),
619 (int)LOG_SAVE,
620 wmsg(/*Save Log*/446).c_str()));
621 wxEndBusyCursor();
622 int retval = pclose(cavern_out);
623 cavern_out = NULL;
624 if (retval) {
625 /* TRANSLATORS: Label for button in aven’s cavern log window which
626 * causes the survey data to be reprocessed. */
627 AppendToPage(wxString::Format(wxT("<avenbutton default id=%d name=\"%s\">"),
628 (int)LOG_REPROCESS,
629 wmsg(/*Reprocess*/184).c_str()));
630 if (retval == -1) {
631 wxString m = wxT("Problem running cavern: ");
632 m += wxString(strerror(errno), wxConvUTF8);
633 wxGetApp().ReportError(m);
634 return;
636 return;
638 AppendToPage(wxString::Format(wxT("<avenbutton id=%d name=\"%s\">"),
639 (int)LOG_REPROCESS,
640 wmsg(/*Reprocess*/184).c_str()));
641 AppendToPage(wxString::Format(wxT("<avenbutton default id=%d>"), (int)wxID_OK));
642 Update();
643 init_done = false;
645 wxString file3d(filename, 0, filename.length() - 3);
646 file3d.append(wxT("3d"));
647 if (!mainfrm->LoadData(file3d, survey)) {
648 return;
650 if (link_count == 0) {
651 wxCommandEvent dummy;
652 OnOK(dummy);
656 void
657 CavernLogWindow::OnReprocess(wxCommandEvent &)
659 process(filename);
662 void
663 CavernLogWindow::OnSave(wxCommandEvent &)
665 wxString filelog(filename, 0, filename.length() - 3);
666 filelog += wxT("log");
667 AvenAllowOnTop ontop(mainfrm);
668 #ifdef __WXMOTIF__
669 wxString ext(wxT("*.log"));
670 #else
671 /* TRANSLATORS: Log files from running cavern (extension .log) */
672 wxString ext = wmsg(/*Log files*/447);
673 ext += wxT("|*.log");
674 #endif
675 wxFileDialog dlg(this, wmsg(/*Select an output filename*/319),
676 wxString(), filelog, ext,
677 wxFD_SAVE|wxFD_OVERWRITE_PROMPT);
678 if (dlg.ShowModal() != wxID_OK) return;
679 filelog = dlg.GetPath();
680 FILE * fh_log = wxFopen(filelog, wxT("w"));
681 if (!fh_log) {
682 wxGetApp().ReportError(wxString::Format(wmsg(/*Error writing to file “%s”*/110), filelog.c_str()));
683 return;
685 fwrite(log_txt.data(), log_txt.size(), 1, fh_log);
686 fclose(fh_log);
689 void
690 CavernLogWindow::OnOK(wxCommandEvent &)
692 if (init_done) {
693 mainfrm->HideLog(this);
694 } else {
695 mainfrm->InitialiseAfterLoad(filename);
696 init_done = true;