2 * Run cavern inside an Aven window
4 * Copyright (C) 2005-2022 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 #include <wx/process.h>
45 #define GVIM_COMMAND "gvim +'call cursor($l,$c)' $f"
46 #define VIM_COMMAND "x-terminal-emulator -e vim +'call cursor($l,$c)' $f"
47 #define NVIM_COMMAND "x-terminal-emulator -e nvim +'call cursor($l,$c)' $f"
48 #define GEDIT_COMMAND "gedit $f +$l:$c"
49 // Pluma currently ignores the column, but include it assuming some future
50 // version will add support.
51 #define PLUMA_COMMAND "pluma +$l:$c $f"
52 #define EMACS_COMMAND "x-terminal-emulator -e emacs +$l:$c $f"
53 #define NANO_COMMAND "x-terminal-emulator -e nano +$l,$c $f"
54 #define JED_COMMAND "x-terminal-emulator -e jed $f -g $l"
55 #define KATE_COMMAND "kate -l $l -c $c $f"
58 # define DEFAULT_EDITOR_COMMAND "notepad $f"
59 #elif defined __WXMAC__
60 # define DEFAULT_EDITOR_COMMAND "open -t $f"
62 # define DEFAULT_EDITOR_COMMAND VIM_COMMAND
65 enum { LOG_REPROCESS
= 1234, LOG_SAVE
= 1235 };
67 static const wxString
badutf8_html(
68 wxT("<span style=\"color:white;background-color:red;\">�</span>"));
69 static const wxString
badutf8(wxUniChar(0xfffd));
71 // New event type for passing a chunk of cavern output from the worker thread
72 // to the main thread (or from the idle event handler if we're not using
74 class CavernOutputEvent
;
76 wxDEFINE_EVENT(wxEVT_CAVERN_OUTPUT
, CavernOutputEvent
);
78 class CavernOutputEvent
: public wxEvent
{
82 CavernOutputEvent() : wxEvent(0, wxEVT_CAVERN_OUTPUT
), len(0) { }
84 wxEvent
* Clone() const {
85 CavernOutputEvent
* e
= new CavernOutputEvent();
87 if (len
> 0) memcpy(e
->buf
, buf
, len
);
92 #ifdef CAVERNLOG_USE_THREADS
93 class CavernThread
: public wxThread
{
95 virtual ExitCode
Entry();
97 CavernLogWindow
*handler
;
102 CavernThread(CavernLogWindow
*handler_
, wxInputStream
* in_
)
103 : wxThread(wxTHREAD_DETACHED
), handler(handler_
), in(in_
) { }
106 wxCriticalSectionLocker
enter(handler
->thread_lock
);
107 handler
->thread
= NULL
;
112 CavernThread::Entry()
115 CavernOutputEvent
* e
= new CavernOutputEvent();
116 in
->Read(e
->buf
, sizeof(e
->buf
));
117 size_t n
= in
->LastRead();
118 if (n
== 0 || TestDestroy()) {
120 return (wxThread::ExitCode
)0;
122 if (n
== 1 && e
->buf
[0] == '\n') {
123 // Don't send an event with just a blank line in.
124 in
->Read(e
->buf
+ 1, sizeof(e
->buf
) - 1);
128 return (wxThread::ExitCode
)0;
132 handler
->QueueEvent(e
);
139 CavernLogWindow::OnIdle(wxIdleEvent
& event
)
141 if (cavern_out
== NULL
) return;
143 wxInputStream
* in
= cavern_out
->GetInputStream();
145 if (!in
->CanRead()) {
146 // Avoid a tight busy-loop on idle events.
150 CavernOutputEvent
* e
= new CavernOutputEvent();
151 in
->Read(e
->buf
, sizeof(e
->buf
));
152 size_t n
= in
->LastRead();
165 BEGIN_EVENT_TABLE(CavernLogWindow
, wxHtmlWindow
)
166 EVT_BUTTON(LOG_REPROCESS
, CavernLogWindow::OnReprocess
)
167 EVT_BUTTON(LOG_SAVE
, CavernLogWindow::OnSave
)
168 EVT_BUTTON(wxID_OK
, CavernLogWindow::OnOK
)
169 EVT_COMMAND(wxID_ANY
, wxEVT_CAVERN_OUTPUT
, CavernLogWindow::OnCavernOutput
)
170 #ifdef CAVERNLOG_USE_THREADS
171 EVT_CLOSE(CavernLogWindow::OnClose
)
173 EVT_IDLE(CavernLogWindow::OnIdle
)
175 EVT_END_PROCESS(wxID_ANY
, CavernLogWindow::OnEndProcess
)
178 wxString
escape_for_shell(wxString s
, bool protect_dash
)
181 // Correct quoting rules are insane:
183 // http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx
185 // Thankfully wxExecute passes the command string to CreateProcess(), so
186 // at least we don't need to quote for cmd.exe too.
187 if (s
.empty() || s
.find_first_of(wxT(" \"\t\n\v")) != s
.npos
) {
189 s
.insert(0, wxT('"'));
190 for (size_t p
= 1; p
< s
.size(); ++p
) {
191 size_t backslashes
= 0;
192 while (s
[p
] == wxT('\\')) {
194 if (++p
== s
.size()) {
195 // Escape all the backslashes, since they're before
196 // the closing quote we add below.
197 s
.append(backslashes
, wxT('\\'));
202 if (s
[p
] == wxT('"')) {
203 // Escape any preceding backslashes and this quote.
204 s
.insert(p
, backslashes
+ 1, wxT('\\'));
205 p
+= backslashes
+ 1;
213 if (protect_dash
&& !s
.empty() && s
[0u] == '-') {
214 // If the filename starts with a '-', protect it from being
215 // treated as an option by prepending "./".
216 s
.insert(0, wxT("./"));
219 while (p
< s
.size()) {
220 // Exclude a few safe characters which are common in filenames
221 if (!isalnum((unsigned char)s
[p
]) && strchr("/._-", s
[p
]) == NULL
) {
222 s
.insert(p
, 1, wxT('\\'));
231 wxString
get_command_path(const wxChar
* command_name
)
240 buf
= (wchar_t*)osrealloc(buf
, len
* 2);
241 got
= GetModuleFileNameW(NULL
, buf
, len
);
242 if (got
< len
) break;
245 /* Strange Win32 nastiness - strip prefix "\\?\" if present */
246 wchar_t *start
= buf
;
247 if (wcsncmp(start
, L
"\\\\?\\", 4) == 0) start
+= 4;
248 wchar_t * slash
= wcsrchr(start
, L
'\\');
250 cmd
.assign(start
, slash
- start
+ 1);
255 wxString cmd
= wxString(msg_exepth(), wxConvUTF8
);
261 CavernLogWindow::CavernLogWindow(MainFrm
* mainfrm_
, const wxString
& survey_
, wxWindow
* parent
)
262 : wxHtmlWindow(parent
),
264 end(buf
), survey(survey_
)
266 int fsize
= parent
->GetFont().GetPointSize();
267 int sizes
[7] = { fsize
, fsize
, fsize
, fsize
, fsize
, fsize
, fsize
};
268 SetFonts(wxString(), wxString(), sizes
);
271 CavernLogWindow::~CavernLogWindow()
273 #ifdef CAVERNLOG_USE_THREADS
274 if (thread
) stop_thread();
278 cavern_out
->Detach();
282 #ifdef CAVERNLOG_USE_THREADS
284 CavernLogWindow::stop_thread()
286 // Killing the subprocess by its pid is theoretically racy, but in practice
287 // it's not going to cause issues, and it's all the wxProcess API seems to
288 // allow us to do. If we don't kill the subprocess, we need to wait for it
289 // to write out some output - there seems to be no way to do the equivalent
290 // of select() with a timeout on a wxInputStream.
292 // The only alternative to this seems to be to do:
294 // while (!s.CanRead()) {
295 // if (TestDestroy()) return (wxThread::ExitCode)0;
299 // But that makes the log window update sluggishly, and we're using a
300 // worker thread precisely to try to avoid having to do dumb stuff like
302 wxProcess::Kill(cavern_out
->GetPid());
305 wxCriticalSectionLocker
enter(thread_lock
);
308 res
= thread
->Delete(NULL
, wxTHREAD_WAIT_BLOCK
);
309 if (res
!= wxTHREAD_NO_ERROR
) {
315 // Wait for thread to complete.
318 wxCriticalSectionLocker
enter(thread_lock
);
326 CavernLogWindow::OnClose(wxCloseEvent
&)
328 if (thread
) stop_thread();
334 CavernLogWindow::OnLinkClicked(const wxHtmlLinkInfo
&link
)
336 wxString href
= link
.GetHref();
337 wxString title
= link
.GetTarget();
338 size_t colon2
= href
.rfind(wxT(':'));
339 if (colon2
== wxString::npos
)
341 size_t colon
= href
.rfind(wxT(':'), colon2
- 1);
342 if (colon
== wxString::npos
)
345 wxChar
* p
= wxGetenv(wxT("SURVEXEDITOR"));
348 if (!cmd
.find(wxT("$f"))) {
352 p
= wxGetenv(wxT("VISUAL"));
353 if (!p
) p
= wxGetenv(wxT("EDITOR"));
355 cmd
= wxT(DEFAULT_EDITOR_COMMAND
);
359 cmd
= wxT(GVIM_COMMAND
);
360 } else if (cmd
== "vim") {
361 cmd
= wxT(VIM_COMMAND
);
362 } else if (cmd
== "nvim") {
363 cmd
= wxT(NVIM_COMMAND
);
364 } else if (cmd
== "gedit") {
365 cmd
= wxT(GEDIT_COMMAND
);
366 } else if (cmd
== "pluma") {
367 cmd
= wxT(PLUMA_COMMAND
);
368 } else if (cmd
== "emacs") {
369 cmd
= wxT(EMACS_COMMAND
);
370 } else if (cmd
== "nano") {
371 cmd
= wxT(NANO_COMMAND
);
372 } else if (cmd
== "jed") {
373 cmd
= wxT(JED_COMMAND
);
374 } else if (cmd
== "kate") {
375 cmd
= wxT(KATE_COMMAND
);
378 cmd
.Replace(wxT("$"), wxT("$$"));
384 while ((i
= cmd
.find(wxT('$'), i
)) != wxString::npos
) {
385 if (++i
>= cmd
.size()) break;
386 switch ((int)cmd
[i
]) {
391 wxString f
= escape_for_shell(href
.substr(0, colon
), true);
392 cmd
.replace(i
- 1, 2, f
);
397 wxString t
= escape_for_shell(title
);
398 cmd
.replace(i
- 1, 2, t
);
403 wxString l
= escape_for_shell(href
.substr(colon
+ 1, colon2
- colon
- 1));
404 cmd
.replace(i
- 1, 2, l
);
410 if (colon2
>= href
.size() - 1)
413 l
= escape_for_shell(href
.substr(colon2
+ 1));
414 cmd
.replace(i
- 1, 2, l
);
423 if (wxExecute(cmd
, wxEXEC_ASYNC
|wxEXEC_MAKE_GROUP_LEADER
) >= 0)
427 // TRANSLATORS: %s is replaced by the command we attempted to run.
428 m
.Printf(wmsg(/*Couldn’t run external command: “%s”*/17), cmd
.c_str());
430 m
+= wxString(strerror(errno
), wxConvUTF8
);
432 wxGetApp().ReportError(m
);
436 CavernLogWindow::process(const wxString
&file
)
439 #ifdef CAVERNLOG_USE_THREADS
440 if (thread
) stop_thread();
443 cavern_out
->Detach();
458 SetEnvironmentVariable(wxT("SURVEX_UTF8"), wxT("1"));
460 setenv("SURVEX_UTF8", "1", 1);
463 wxString escaped_file
= escape_for_shell(file
, true);
464 wxString cmd
= get_command_path(L
"cavern");
465 cmd
= escape_for_shell(cmd
, false);
471 cavern_out
= wxProcess::Open(cmd
);
474 m
.Printf(wmsg(/*Couldn’t run external command: “%s”*/17), cmd
.c_str());
476 m
+= wxString(strerror(errno
), wxConvUTF8
);
478 wxGetApp().ReportError(m
);
482 // We want to receive the wxProcessEvent when cavern exits.
483 cavern_out
->SetNextHandler(this);
485 #ifdef CAVERNLOG_USE_THREADS
486 thread
= new CavernThread(this, cavern_out
->GetInputStream());
487 if (thread
->Run() != wxTHREAD_NO_ERROR
) {
488 wxGetApp().ReportError(wxT("Thread failed to start"));
496 CavernLogWindow::OnCavernOutput(wxCommandEvent
& e_
)
498 CavernOutputEvent
& e
= (CavernOutputEvent
&)e_
;
502 if (size_t(n
) > sizeof(buf
) - (end
- buf
)) abort();
503 memcpy(end
, e
.buf
, n
);
504 log_txt
.append((const char *)end
, n
);
507 const unsigned char * p
= buf
;
512 // Decode multi-byte UTF-8 sequence.
514 // Invalid UTF-8 sequence.
516 } else if (ch
< 0xe0) {
517 /* 2 byte sequence */
519 // Incomplete UTF-8 sequence - try to read more.
523 if ((ch1
& 0xc0) != 0x80) {
524 // Invalid UTF-8 sequence.
527 ch
= ((ch
& 0x1f) << 6) | (ch1
& 0x3f);
528 } else if (ch
< 0xf0) {
529 /* 3 byte sequence */
531 // Incomplete UTF-8 sequence - try to read more.
535 ch
= ((ch
& 0x1f) << 12) | ((ch1
& 0x3f) << 6);
536 if ((ch1
& 0xc0) != 0x80) {
537 // Invalid UTF-8 sequence.
541 if ((ch2
& 0xc0) != 0x80) {
542 // Invalid UTF-8 sequence.
547 // Overlong UTF-8 sequence.
555 // Resync to next byte which starts a UTF-8 sequence.
557 if (*p
< 0x80 || (*p
>= 0xc0 && *p
< 0xf0)) break;
569 if (cur
.empty()) continue;
571 if (source_line
.empty()) {
572 // Source line shown for context. Store it so we
573 // can use the caret line to highlight it.
574 swap(source_line
, cur
);
576 size_t caret
= cur
.rfind('^');
577 if (caret
!= wxString::npos
) {
578 size_t tilde
= cur
.rfind('~');
579 if (tilde
== wxString::npos
|| tilde
< caret
) {
583 // FIXME: Need to count each & entity as one character...
584 cur
.append(source_line
, 1, caret
- 1);
585 if (caret
< source_line
.size()) {
587 cur
.append(highlight
? highlight
: wxT("<span \"color:blue\">"));
588 cur
.append(source_line
, caret
, tilde
+ 1 - caret
);
589 cur
.append("</span></b>");
591 if (tilde
+ 1 < source_line
.size()) {
592 cur
.append(source_line
, tilde
+ 1, wxString::npos
);
595 // No caret in second line - just output both.
596 source_line
.replace(0, 1, " ");
597 source_line
+= "<br>\n ";
598 source_line
.append(cur
, 1, wxString::npos
);
599 swap(cur
, source_line
);
609 if (!source_line
.empty()) {
610 // Previous line was a source line without column info
612 source_line
.replace(0, 1, " ");
613 source_line
+= "<br>\n";
614 AppendToPage(source_line
);
618 size_t colon
= cur
.find(':');
620 // If the path is "C:\path\to\file.svx" then don't split at the
621 // : after the drive letter! FIXME: better to look for ": "?
622 size_t colon
= cur
.find(':', 2);
624 if (colon
!= wxString::npos
&& colon
< cur
.size() - 2) {
627 while (i
< cur
.size() - 2 &&
628 cur
[i
] >= wxT('0') && cur
[i
] <= wxT('9')) {
631 if (i
> colon
&& cur
[i
] == wxT(':') ) {
633 // Check for column number.
634 while (++i
< cur
.size() - 2 &&
635 cur
[i
] >= wxT('0') && cur
[i
] <= wxT('9')) { }
636 bool have_column
= (i
> colon
+ 1 && cur
[i
] == wxT(':'));
640 // If there's no colon, include a trailing ':'
641 // so that we can unambiguously split the href
642 // value up into filename, line and column.
645 wxString tag
= wxT("<a href=\"");
646 tag
.append(cur
, 0, colon
);
647 while (cur
[++i
] == wxT(' ')) { }
648 tag
+= wxT("\" target=\"");
649 wxString
target(cur
, i
, wxString::npos
);
650 target
.Replace(badutf8_html
, badutf8
);
654 size_t offset
= colon
+ tag
.size();
655 cur
.insert(offset
, wxT("</a>"));
658 if (!have_column
) --offset
;
660 static const wxString
& error_marker
= wmsg(/*error*/93) + ":";
661 static const wxString
& warning_marker
= wmsg(/*warning*/4) + ":";
662 static const wxString
& info_marker
= wmsg(/*info*/485) + ":";
664 if (cur
.substr(offset
, error_marker
.size()) == error_marker
) {
665 // Show "error" marker in red.
666 highlight
= wxT("<span style=\"color:red\">");
667 cur
.insert(offset
, highlight
);
668 offset
+= 24 + error_marker
.size() - 1;
669 cur
.insert(offset
, wxT("</span>"));
670 } else if (cur
.substr(offset
, warning_marker
.size()) == warning_marker
) {
671 // Show "warning" marker in orange.
672 highlight
= wxT("<span style=\"color:orange\">");
673 cur
.insert(offset
, highlight
);
674 offset
+= 27 + warning_marker
.size() - 1;
675 cur
.insert(offset
, wxT("</span>"));
676 } else if (cur
.substr(offset
, info_marker
.size()) == info_marker
) {
677 // Show "info" marker in blue.
679 highlight
= wxT("<span style=\"color:blue\">");
680 cur
.insert(offset
, highlight
);
681 offset
+= 25 + info_marker
.size() - 1;
682 cur
.insert(offset
, wxT("</span>"));
691 // Save the scrollbar positions.
692 int scroll_x
= 0, scroll_y
= 0;
693 GetViewStart(&scroll_x
, &scroll_y
);
695 cur
+= wxT("<br>\n");
699 // Auto-scroll the window until we've reported a
702 GetVirtualSize(&x
, &y
);
704 GetClientSize(&xs
, &ys
);
707 GetScrollPixelsPerUnit(&xu
, &yu
);
708 Scroll(scroll_x
, y
/ yu
);
710 // Restore the scrollbar positions.
711 Scroll(scroll_x
, scroll_y
);
733 // This approach means that highlighting of "error" or
734 // "warning" won't work in translations where they contain
735 // non-ASCII characters, but wxWidgets >= 3.0 in always
736 // Unicode, so this corner case is already very uncommon,
737 // and will become irrelevant with time.
739 cur
+= wxString::Format(wxT("&#%u;"), ch
);
747 size_t left
= end
- p
;
749 if (left
) memmove(buf
, p
, left
);
754 if (!source_line
.empty()) {
755 // Previous line was a source line without column info
757 source_line
.replace(0, 1, " ");
758 source_line
+= "<br>\n";
759 AppendToPage(source_line
);
763 if (e
.len
<= 0 && buf
!= end
) {
764 // Truncated UTF-8 sequence.
769 AppendToPage("<hr>" + cur
);
772 /* TRANSLATORS: Label for button in aven’s cavern log window which
773 * allows the user to save the log to a file. */
774 AppendToPage(wxString::Format(wxT("<avenbutton id=%d name=\"%s\">"),
776 wmsg(/*&Save Log*/446).c_str()));
781 /* Negative length indicates non-zero exit status from cavern. */
782 /* TRANSLATORS: Label for button in aven’s cavern log window which
783 * causes the survey data to be reprocessed. */
784 AppendToPage(wxString::Format(wxT("<avenbutton default id=%d name=\"%s\">"),
786 wmsg(/*&Reprocess*/184).c_str()));
789 AppendToPage(wxString::Format(wxT("<avenbutton id=%d name=\"%s\">"),
791 wmsg(/*&Reprocess*/184).c_str()));
792 AppendToPage(wxString::Format(wxT("<avenbutton default id=%d>"), (int)wxID_OK
));
797 wxString
file3d(filename
, 0, filename
.length() - 3);
798 file3d
.append(wxT("3d"));
799 if (!mainfrm
->LoadData(file3d
, survey
)) {
804 // Don't stay on log if there there are only "info" diagnostics.
805 if (link_count
== info_count
) {
806 wxCommandEvent dummy
;
812 CavernLogWindow::OnEndProcess(wxProcessEvent
& evt
)
814 CavernOutputEvent
* e
= new CavernOutputEvent();
815 // Zero length indicates successful exit, negative length unsuccessful exit.
816 e
->len
= (evt
.GetExitCode() == 0 ? 0 : -1);
821 CavernLogWindow::OnReprocess(wxCommandEvent
&)
827 CavernLogWindow::OnSave(wxCommandEvent
&)
829 wxString
filelog(filename
, 0, filename
.length() - 3);
830 filelog
+= wxT("log");
832 wxString
ext(wxT("*.log"));
834 /* TRANSLATORS: Log files from running cavern (extension .log) */
835 wxString ext
= wmsg(/*Log files*/447);
836 ext
+= wxT("|*.log");
838 wxFileDialog
dlg(this, wmsg(/*Select an output filename*/319),
839 wxString(), filelog
, ext
,
840 wxFD_SAVE
|wxFD_OVERWRITE_PROMPT
);
841 if (dlg
.ShowModal() != wxID_OK
) return;
842 filelog
= dlg
.GetPath();
843 FILE * fh_log
= wxFopen(filelog
, wxT("w"));
845 wxGetApp().ReportError(wxString::Format(wmsg(/*Error writing to file “%s”*/110), filelog
.c_str()));
848 fwrite(log_txt
.data(), log_txt
.size(), 1, fh_log
);
853 CavernLogWindow::OnOK(wxCommandEvent
&)
856 mainfrm
->HideLog(this);
858 mainfrm
->InitialiseAfterLoad(filename
, survey
);