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
26 #include "cavernlog.h"
36 #ifdef HAVE_SYS_SELECT_H
37 #include <sys/select.h>
40 #include <sys/types.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
{
56 CavernOutputEvent() : wxEvent(0, wxEVT_CAVERN_OUTPUT
), len(0) { }
58 wxEvent
* Clone() const {
59 CavernOutputEvent
* e
= new CavernOutputEvent();
61 if (len
> 0) memcpy(e
->buf
, buf
, len
);
66 class CavernThread
: public wxThread
{
68 virtual ExitCode
Entry();
70 CavernLogWindow
*handler
;
75 CavernThread(CavernLogWindow
*handler_
, int fd
)
76 : wxThread(wxTHREAD_DETACHED
), handler(handler_
), cavern_fd(fd
) { }
79 wxCriticalSectionLocker
enter(handler
->thread_lock
);
80 handler
->thread
= NULL
;
89 CavernOutputEvent
* e
= new CavernOutputEvent();
90 e
->len
= n
= read(cavern_fd
, e
->buf
, sizeof(e
->buf
));
95 wxQueueEvent(handler
, e
);
97 return (wxThread::ExitCode
)0;
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
)
109 EVT_IDLE(CavernLogWindow::OnIdle
)
113 static wxString
escape_for_shell(wxString s
, bool protect_dash
= false)
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()) {
124 if (ch
== wxT('"')) {
125 s
.insert(p
, wxT("\\^"));
128 } else if (ch
== ' ') {
130 } else if (strchr("()%<>&|^", ch
)) {
131 s
.insert(p
, wxT('^'));
138 s
.insert(0u, wxT("^\""));
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("./"));
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('\\'));
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
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()
180 #ifdef CAVERNLOG_USE_THREADS
182 CavernLogWindow::stop_thread()
185 wxCriticalSectionLocker
enter(thread_lock
);
188 #if wxCHECK_VERSION(2,9,2)
189 res
= thread
->Delete(NULL
, wxTHREAD_WAIT_BLOCK
);
191 res
= thread
->Delete();
193 if (res
!= wxTHREAD_NO_ERROR
) {
199 // Wait for thread to complete.
202 wxCriticalSectionLocker
enter(thread_lock
);
210 CavernLogWindow::OnClose(wxCloseEvent
&)
212 if (thread
) stop_thread();
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
)
225 size_t colon
= href
.rfind(wxT(':'), colon2
- 1);
226 if (colon
== wxString::npos
)
229 wxString cmd
= wxT("notepad $f");
230 #elif defined __WXMAC__
231 wxString cmd
= wxT("open -t $f");
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");
239 wxChar
* p
= wxGetenv(wxT("SURVEXEDITOR"));
242 if (!cmd
.find(wxT("$f"))) {
247 while ((i
= cmd
.find(wxT('$'), i
)) != wxString::npos
) {
248 if (++i
>= cmd
.size()) break;
249 switch ((int)cmd
[i
]) {
254 wxString f
= escape_for_shell(href
.substr(0, colon
), true);
255 cmd
.replace(i
- 1, 2, f
);
260 wxString t
= escape_for_shell(title
);
261 cmd
.replace(i
- 1, 2, t
);
266 wxString l
= escape_for_shell(href
.substr(colon
+ 1, colon2
- colon
- 1));
267 cmd
.replace(i
- 1, 2, l
);
273 if (colon2
>= href
.size())
276 l
= escape_for_shell(href
.substr(colon2
+ 1));
277 cmd
.replace(i
- 1, 2, l
);
285 if (wxSystem(cmd
) >= 0)
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());
291 m
+= wxString(strerror(errno
), wxConvUTF8
);
293 wxGetApp().ReportError(m
);
297 CavernLogWindow::process(const wxString
&file
)
300 #ifdef CAVERNLOG_USE_THREADS
301 if (thread
) stop_thread();
318 SetEnvironmentVariable(wxT("SURVEX_UTF8"), wxT("1"));
320 setenv("SURVEX_UTF8", "1", 1);
323 wxString escaped_file
= escape_for_shell(file
, true);
331 buf
= (wchar_t*)osrealloc(buf
, len
* 2);
332 got
= GetModuleFileNameW(NULL
, buf
, len
);
333 if (got
< len
) break;
336 /* Strange Win32 nastiness - strip prefix "\\?\" if present */
337 if (wcsncmp(buf
, L
"\\\\?\\", 4) == 0) buf
+= 4;
338 wchar_t * slash
= wcsrchr(buf
, L
'\\');
340 cmd
.assign(buf
, slash
- buf
+ 1);
344 cmd
= escape_for_shell(cmd
, false);
346 char *cavern
= use_path(msg_exepth(), "cavern");
347 wxString cmd
= escape_for_shell(wxString(cavern
, wxConvUTF8
), false);
356 cavern_out
= _wpopen(cmd
.c_str(), L
"r");
358 cavern_out
= popen(cmd
.mb_str(), "r");
362 m
.Printf(wmsg(/*Couldn’t run external command: “%s”*/17), cmd
.c_str());
364 m
+= wxString(strerror(errno
), wxConvUTF8
);
366 wxGetApp().ReportError(m
);
369 #ifndef CAVERNLOG_USE_THREADS
373 CavernLogWindow::OnIdle(wxIdleEvent
& event
)
375 if (cavern_out
== NULL
) return;
380 cavern_fd
= _fileno(cavern_out
);
382 cavern_fd
= fileno(cavern_out
);
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"));
394 CavernLogWindow::OnCavernOutput(wxCommandEvent
& e_
)
396 CavernOutputEvent
& e
= (CavernOutputEvent
&)e_
;
400 if (size_t(n
) > sizeof(buf
) - (end
- buf
)) abort();
401 memcpy(end
, e
.buf
, n
);
405 assert(cavern_fd
< FD_SETSIZE
); // FIXME we shouldn't just assert, but what else to do?
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
;
415 timeout
.tv_usec
= 10000;
416 int r
= select(cavern_fd
+ 1, &rfds
, NULL
, &efds
, &timeout
);
418 // No new output to process.
422 if (r
<= 0 || !FD_ISSET(cavern_fd
, &rfds
))
425 n
= read(cavern_fd
, end
, sizeof(buf
) - (end
- buf
));
428 log_txt
.append((const char *)end
, n
);
431 const unsigned char * p
= buf
;
436 // Decode multi-byte UTF-8 sequence.
438 // Invalid UTF-8 sequence.
440 } else if (ch
< 0xe0) {
441 /* 2 byte sequence */
443 // Incomplete UTF-8 sequence - try to read more.
447 if ((ch1
& 0xc0) != 0x80) {
448 // Invalid UTF-8 sequence.
451 ch
= ((ch
& 0x1f) << 6) | (ch1
& 0x3f);
452 } else if (ch
< 0xf0) {
453 /* 3 byte sequence */
455 // Incomplete UTF-8 sequence - try to read more.
459 ch
= ((ch
& 0x1f) << 12) | ((ch1
& 0x3f) << 6);
460 if ((ch1
& 0xc0) != 0x80) {
461 // Invalid UTF-8 sequence.
465 if ((ch2
& 0xc0) != 0x80) {
466 // Invalid UTF-8 sequence.
471 // Overlong UTF-8 sequence.
481 if (cur
.empty()) continue;
483 size_t colon
= cur
.find(':');
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);
489 if (colon
!= wxString::npos
&& colon
< cur
.size() - 2) {
492 while (i
< cur
.size() - 2 &&
493 cur
[i
] >= wxT('0') && cur
[i
] <= wxT('9')) {
496 if (i
> colon
&& cur
[i
] == wxT(':') ) {
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(':') ) {
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.
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
);
516 size_t offset
= colon
+ tag
.size();
517 cur
.insert(offset
, wxT("</a>"));
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>"));
539 // Save the scrollbar positions.
540 int scroll_x
= 0, scroll_y
= 0;
541 GetViewStart(&scroll_x
, &scroll_y
);
543 cur
+= wxT("<br>\n");
547 // Auto-scroll the window until we've reported a
550 GetVirtualSize(&x
, &y
);
552 GetClientSize(&xs
, &ys
);
555 GetScrollPixelsPerUnit(&xu
, &yu
);
556 Scroll(scroll_x
, y
/ yu
);
558 // Restore the scrollbar positions.
559 Scroll(scroll_x
, scroll_y
);
579 cur
+= wxString::Format(wxT("&#%u;"), ch
);
586 size_t left
= end
- p
;
588 if (left
) memmove(buf
, p
, left
);
590 #ifndef CAVERNLOG_USE_THREADS
596 #ifdef CAVERNLOG_USE_THREADS
597 if (e
.len
== 0 && buf
!= end
) {
598 // Truncated UTF-8 sequence.
602 if (n
== 0 && buf
!= end
) {
603 // Truncated UTF-8 sequence.
612 #if !defined CAVERNLOG_USE_THREADS && !defined __WXMSW__
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\">"),
620 wmsg(/*Save Log*/446).c_str()));
622 int retval
= pclose(cavern_out
);
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\">"),
629 wmsg(/*Reprocess*/184).c_str()));
631 wxString m
= wxT("Problem running cavern: ");
632 m
+= wxString(strerror(errno
), wxConvUTF8
);
633 wxGetApp().ReportError(m
);
638 AppendToPage(wxString::Format(wxT("<avenbutton id=%d name=\"%s\">"),
640 wmsg(/*Reprocess*/184).c_str()));
641 AppendToPage(wxString::Format(wxT("<avenbutton default id=%d>"), (int)wxID_OK
));
645 wxString
file3d(filename
, 0, filename
.length() - 3);
646 file3d
.append(wxT("3d"));
647 if (!mainfrm
->LoadData(file3d
, survey
)) {
650 if (link_count
== 0) {
651 wxCommandEvent dummy
;
657 CavernLogWindow::OnReprocess(wxCommandEvent
&)
663 CavernLogWindow::OnSave(wxCommandEvent
&)
665 wxString
filelog(filename
, 0, filename
.length() - 3);
666 filelog
+= wxT("log");
667 AvenAllowOnTop
ontop(mainfrm
);
669 wxString
ext(wxT("*.log"));
671 /* TRANSLATORS: Log files from running cavern (extension .log) */
672 wxString ext
= wmsg(/*Log files*/447);
673 ext
+= wxT("|*.log");
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"));
682 wxGetApp().ReportError(wxString::Format(wmsg(/*Error writing to file “%s”*/110), filelog
.c_str()));
685 fwrite(log_txt
.data(), log_txt
.size(), 1, fh_log
);
690 CavernLogWindow::OnOK(wxCommandEvent
&)
693 mainfrm
->HideLog(this);
695 mainfrm
->InitialiseAfterLoad(filename
);