1 /*
2 * log_uploader.cpp
3 * PHD Guiding
4 *
5 * Created by Andy Galasso
6 * Copyright (c) 2018 Andy Galasso
7 * All rights reserved.
8 *
9 * This source code is distributed under the following "BSD" license
10 * Redistribution and use in source and binary forms, with or without
11 * modification, are permitted provided that the following conditions are met:
12 * Redistributions of source code must retain the above copyright notice,
13 * this list of conditions and the following disclaimer.
14 * Redistributions in binary form must reproduce the above copyright notice,
15 * this list of conditions and the following disclaimer in the
16 * documentation and/or other materials provided with the distribution.
17 * Neither the name of Open PHD Guiding, openphdguiding.org, nor the names of its
18 * contributors may be used to endorse or promote products derived from
19 * this software without specific prior written permission.
20 *
21 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
25 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
26 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
27 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
28 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
29 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
30 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 * POSSIBILITY OF SUCH DAMAGE.
32 *
33 */
34
35 #include "log_uploader.h"
36 #include "phd.h"
37
38 #include <algorithm>
39 #include <curl/curl.h>
40 #include <fstream>
41 #include <sstream>
42 #include <wx/clipbrd.h>
43 #include <wx/dir.h>
44 #include <wx/hyperlink.h>
45 #include <wx/regex.h>
46 #include <wx/richtooltip.h>
47 #include <wx/tokenzr.h>
48 #include <wx/wfstream.h>
49 #include <wx/zipstrm.h>
50
51 #if LIBCURL_VERSION_MAJOR < 7 || (LIBCURL_VERSION_MAJOR == 7 && LIBCURL_VERSION_MINOR < 32)
52 # define OLD_CURL
53 #endif
54
55 #ifdef _MSC_VER
56 # define strncasecmp strnicmp
57 #endif
58
59 struct WindowUpdateLocker
60 {
61 wxWindow *m_win;
WindowUpdateLockerWindowUpdateLocker62 WindowUpdateLocker(wxWindow *win) : m_win(win) { win->Freeze(); }
~WindowUpdateLockerWindowUpdateLocker63 ~WindowUpdateLocker()
64 {
65 m_win->Thaw();
66 m_win->Refresh();
67 }
68 };
69
70 enum SummaryState
71 {
72 ST_BEGIN,
73 ST_LOADING,
74 ST_LOADED,
75 };
76
77 struct Session
78 {
79 wxString timestamp;
80 wxDateTime start;
81 GuideLogSummaryInfo summary;
82 SummaryState summary_loaded = ST_BEGIN;
83 bool has_guide = false;
84 bool has_debug = false;
85
HasGuidingSession86 bool HasGuiding() const
87 {
88 assert(summary_loaded == ST_LOADED);
89 return summary.valid && summary.guide_cnt > 0;
90 }
91 };
92
93 static std::vector<Session> s_session;
94 // grid sort order defined by these maps between grid row and session index
95 static std::vector<int> s_grid_row; // map session index to grid row
96 static std::vector<int> s_session_idx; // map grid row => session index
97
98 static bool s_include_empty;
99
100 enum
101 {
102 COL_SELECT,
103 COL_NIGHTOF,
104 COL_GUIDE,
105 COL_CAL,
106 COL_GA,
107
108 NUM_COLUMNS
109 };
110
111 enum
112 {
113 // always show some rows, otherwise the grid looks weird surrounded by lots of empty space
114 // the grid needs at least 2 rows, otherwise the the bool cell editor and/or renderer do not work properly
115 MIN_ROWS = 16,
116 };
117
118 // state machine to allow scanning logs during idle event processing
119 //
120 struct LogScanner
121 {
122 wxGrid *m_grid;
123 std::deque<int> m_q; // indexes remaining to be checked
124 std::ifstream m_ifs;
125 wxDateTime m_guiding_starts;
126 void Init(wxGrid *grid);
127 void FindNextRow();
128 bool DoWork(unsigned int millis);
129 };
130
Init(wxGrid * grid)131 void LogScanner::Init(wxGrid *grid)
132 {
133 m_grid = grid;
134 // load work queue in sorted order
135 for (auto idx : s_session_idx)
136 {
137 if (s_session[idx].summary_loaded != ST_LOADED)
138 m_q.push_back(idx);
139 }
140 m_guiding_starts = wxInvalidDateTime;
141 FindNextRow();
142 }
143
DebugLogName(const Session & session)144 static wxString DebugLogName(const Session& session)
145 {
146 return "PHD2_DebugLog_" + session.timestamp + ".txt";
147 }
148
GuideLogName(const Session & session)149 static wxString GuideLogName(const Session& session)
150 {
151 return "PHD2_GuideLog_" + session.timestamp + ".txt";
152 }
153
FormatTimeSpan(const wxTimeSpan & dt)154 static wxString FormatTimeSpan(const wxTimeSpan& dt)
155 {
156 int days = dt.GetDays();
157 if (days > 1)
158 return wxString::Format(_("%dd"), days); // 2d or more
159 int hrs = dt.GetHours();
160 if (days == 1)
161 return wxString::Format(_("%dhr"), hrs); // 24-47h
162 // < 24h
163 int mins = dt.GetMinutes();
164 mins -= hrs * 60;
165 if (hrs > 0)
166 return wxString::Format(_("%dhr%dmin"), hrs, mins);
167 // < 1h
168 if (mins > 0)
169 return wxString::Format(_("%dmin"), mins);
170 // < 1min
171 return wxString::Format(_("%dsec"), dt.GetSeconds());
172 }
173
FormatGuideFor(const Session & session)174 static wxString FormatGuideFor(const Session& session)
175 {
176 switch (session.summary_loaded) {
177 case ST_BEGIN: default:
178 return wxEmptyString;
179 case ST_LOADING:
180 return _("loading...");
181 case ST_LOADED:
182 if (session.HasGuiding())
183 {
184 // looks better in the grid with some padding
185 return " " +
186 FormatTimeSpan(wxTimeSpan(0 /* hrs */, 0 /* minutes */, session.summary.guide_dur)) +
187 " ";
188 }
189 else
190 return _("None");
191 }
192 }
193
FillActivity(wxGrid * grid,int row,const Session & session,bool resize)194 static void FillActivity(wxGrid *grid, int row, const Session& session, bool resize)
195 {
196 grid->SetCellValue(row, COL_GUIDE, FormatGuideFor(session));
197
198 if (session.summary.cal_cnt)
199 grid->SetCellValue(row, COL_CAL, "Y");
200
201 if (session.summary.ga_cnt)
202 grid->SetCellValue(row, COL_GA, "Y");
203
204 if (session.summary_loaded != ST_LOADED || session.HasGuiding() || s_include_empty)
205 grid->ShowRow(row);
206 else
207 grid->HideRow(row);
208
209 if (resize)
210 {
211 grid->AutoSizeColumn(COL_GUIDE);
212 grid->AutoSizeColumn(COL_CAL);
213 grid->AutoSizeColumn(COL_GA);
214 }
215 }
216
FindNextRow()217 void LogScanner::FindNextRow()
218 {
219 while (!m_q.empty())
220 {
221 auto idx = m_q.front();
222 Session& session = s_session[idx];
223
224 assert(session.has_guide);
225 assert(session.summary_loaded != ST_LOADED);
226
227 int row = s_grid_row[idx];
228
229 wxFileName fn(Debug.GetLogDir(), GuideLogName(session));
230 m_ifs.open(fn.GetFullPath().fn_str());
231 if (!m_ifs)
232 {
233 // should never get here since we have already scanned the list once
234 session.summary_loaded = ST_LOADED;
235 FillActivity(m_grid, row, session, true);
236 m_q.pop_front();
237 continue;
238 }
239
240 session.summary_loaded = ST_LOADING;
241 FillActivity(m_grid, row, session, true);
242
243 return;
244 }
245 }
246
247 static std::string GUIDING_BEGINS("Guiding Begins at ");
248 static std::string GUIDING_ENDS("Guiding Ends at ");
249 static std::string CALIBRATION_ENDS("Calibration complete");
250 static std::string GA_COMPLETE("INFO: GA Result - Dec Drift Rate=");
251
StartsWith(const std::string & s,const std::string & pfx)252 inline static bool StartsWith(const std::string& s, const std::string& pfx)
253 {
254 return s.length() >= pfx.length() &&
255 s.compare(0, pfx.length(), pfx) == 0;
256 }
257
DoWork(unsigned int millis)258 bool LogScanner::DoWork(unsigned int millis)
259 {
260 int n = 0;
261 wxStopWatch swatch;
262
263 while (!m_q.empty())
264 {
265 auto idx = m_q.front();
266 Session& session = s_session[idx];
267
268 std::string line;
269 while (true)
270 {
271 if (++n % 1000 == 0)
272 {
273 if (swatch.Time() > millis)
274 return true;
275 }
276
277 if (!std::getline(m_ifs, line))
278 break;
279
280 if (StartsWith(line, GUIDING_BEGINS))
281 {
282 std::string datestr = line.substr(GUIDING_BEGINS.length());
283 m_guiding_starts.ParseISOCombined(datestr, ' ');
284 continue;
285 }
286
287 if (StartsWith(line, GUIDING_ENDS) && m_guiding_starts.IsValid())
288 {
289 std::string datestr = line.substr(GUIDING_ENDS.length());
290 wxDateTime end;
291 end.ParseISOCombined(datestr, ' ');
292 if (end.IsValid() && end.IsLaterThan(m_guiding_starts))
293 {
294 wxTimeSpan dt = end - m_guiding_starts;
295 ++session.summary.guide_cnt;
296 session.summary.guide_dur += dt.GetSeconds().GetValue();
297 }
298 m_guiding_starts = wxInvalidDateTime;
299 continue;
300 }
301
302 if (StartsWith(line, CALIBRATION_ENDS))
303 {
304 ++session.summary.cal_cnt;
305 continue;
306 }
307
308 if (StartsWith(line, GA_COMPLETE))
309 {
310 ++session.summary.ga_cnt;
311 continue;
312 }
313 }
314
315 session.summary.valid = true;
316 session.summary_loaded = ST_LOADED;
317
318 FillActivity(m_grid, s_grid_row[idx], session, true);
319
320 m_ifs.close();
321 m_q.pop_front();
322 FindNextRow();
323 }
324
325 return false;
326 }
327
328 class LogUploadDialog : public wxDialog
329 {
330 public:
331 int m_step;
332 int m_nrSelected;
333 wxStaticText *m_text;
334 wxHtmlWindow *m_html;
335 wxGrid *m_grid;
336 wxHyperlinkCtrl *m_recent;
337 wxCheckBox *m_includeEmpty;
338 wxButton *m_back;
339 wxButton *m_upload;
340 LogScanner m_scanner;
341
342 void OnCellLeftClick(wxGridEvent& event);
343 void OnColSort(wxGridEvent& event);
344 void OnRecentClicked(wxHyperlinkEvent& event);
345 void OnBackClick(wxCommandEvent& event);
346 void OnUploadClick(wxCommandEvent& event);
347 void OnLinkClicked(wxHtmlLinkEvent& event);
348 void OnIdle(wxIdleEvent& event);
349 void OnIncludeEmpty(wxCommandEvent& ev);
350
351 void ConfirmUpload();
352 void ExecUpload();
353
354 LogUploadDialog(wxWindow *parent);
355 ~LogUploadDialog();
356 };
357
Val(const wxString & s,size_t start,size_t len)358 inline static wxDateTime::wxDateTime_t Val(const wxString& s, size_t start, size_t len)
359 {
360 unsigned long val = 0;
361 wxString(s, start, len).ToULong(&val);
362 return val;
363 }
364
SessionStart(const wxString & timestamp)365 static wxDateTime SessionStart(const wxString& timestamp)
366 {
367 wxDateTime::wxDateTime_t day = Val(timestamp, 8, 2);
368 wxDateTime::Month month = static_cast<wxDateTime::Month>(wxDateTime::Jan + Val(timestamp, 5, 2) - 1);
369 wxDateTime::wxDateTime_t year = Val(timestamp, 0, 4);
370 wxDateTime::wxDateTime_t hour = Val(timestamp, 11, 2);
371 wxDateTime::wxDateTime_t minute = Val(timestamp, 13, 2);
372 wxDateTime::wxDateTime_t second = Val(timestamp, 15, 2);
373 return wxDateTime(day, month, year, hour, minute, second);
374 }
375
FormatNightOf(const wxDateTime & t)376 static wxString FormatNightOf(const wxDateTime& t)
377 {
378 return t.Format(" %A %x "); // looks better in the grid with some padding
379 }
380
FormatTimestamp(const wxDateTime & t)381 static wxString FormatTimestamp(const wxDateTime& t)
382 {
383 return t.Format("%Y-%m-%d %H:%M:%S");
384 }
385
QuickInitSummary(Session & s)386 static void QuickInitSummary(Session& s)
387 {
388 if (!s.has_guide)
389 {
390 s.summary_loaded = ST_LOADED;
391 return;
392 }
393
394 const wxString& logDir = Debug.GetLogDir();
395 wxFileName fn(logDir, GuideLogName(s));
396
397 wxFFile file(fn.GetFullPath());
398 if (!file.IsOpened())
399 {
400 s.summary_loaded = ST_LOADED;
401 return;
402 }
403
404 s.summary.LoadSummaryInfo(file);
405 if (s.summary.valid)
406 s.summary_loaded = ST_LOADED;
407 }
408
ReallyFlush(const wxFFile & ffile)409 static void ReallyFlush(const wxFFile& ffile)
410 {
411 #ifdef __WINDOWS__
412 // On Windows the Flush() calls made by GuidingLog and DebugLog are not sufficient
413 // to get the changes onto the filesystem without some contortions
414 if (ffile.IsOpened())
415 FlushFileBuffers((HANDLE) _get_osfhandle(_fileno(ffile.fp())));
416 #endif
417 }
418
FlushLogs()419 static void FlushLogs()
420 {
421 ReallyFlush(Debug);
422 ReallyFlush(GuideLog.File());
423 }
424
GetSortCol(wxGrid * grid)425 static int GetSortCol(wxGrid *grid)
426 {
427 int col = -1;
428 for (int i = 0; i < grid->GetNumberCols(); i++)
429 if (grid->IsSortingBy(i)) {
430 col = i;
431 break;
432 }
433 return col;
434 }
435
436 struct SessionCompare
437 {
438 bool asc;
SessionCompareSessionCompare439 SessionCompare(bool asc_) : asc(asc_) { }
operator ()SessionCompare440 bool operator()(int a, int b) const {
441 if (!asc) std::swap(a, b);
442 return s_session[a].start < s_session[b].start;
443 }
444 };
445
DoSort(wxGrid * grid)446 static void DoSort(wxGrid *grid)
447 {
448 if (GetSortCol(grid) != COL_NIGHTOF)
449 return;
450
451 size_t const nr_sessions = s_session.size();
452
453 // grab the selections
454 std::vector<bool> selected(nr_sessions);
455 for (int r = 0; r < nr_sessions; r++)
456 selected[s_session_idx[r]] = !grid->GetCellValue(r, COL_SELECT).IsEmpty();
457
458 // sort row indexes
459 std::sort(s_session_idx.begin(), s_session_idx.end(), SessionCompare(grid->IsSortOrderAscending()));
460
461 // rebuild mapping of indexes to rows
462 for (int r = 0; r < nr_sessions; r++)
463 s_grid_row[s_session_idx[r]] = r;
464
465 // (re)load the grid
466
467 grid->ClearGrid();
468
469 for (int r = 0; r < nr_sessions; r++)
470 {
471 const Session& session = s_session[s_session_idx[r]];
472 grid->SetCellValue(r, COL_NIGHTOF, FormatNightOf(wxGetApp().ImagingDay(session.start)));
473 FillActivity(grid, r, session, false);
474 if (session.has_guide || session.has_debug)
475 {
476 grid->SetCellEditor(r, COL_SELECT, new wxGridCellBoolEditor());
477 grid->SetCellRenderer(r, COL_SELECT, new wxGridCellBoolRenderer());
478 grid->SetCellValue(r, COL_SELECT, selected[s_session_idx[r]] ? "1" : "");
479 grid->SetReadOnly(r, COL_SELECT, false);
480 }
481 else
482 {
483 grid->SetCellEditor(r, COL_SELECT, grid->GetDefaultEditor());
484 grid->SetCellRenderer(r, COL_SELECT, grid->GetDefaultRenderer());
485 grid->SetReadOnly(r, COL_SELECT, true);
486 }
487 }
488 }
489
LoadGrid(wxGrid * grid)490 static void LoadGrid(wxGrid *grid)
491 {
492 wxBusyCursor spin;
493
494 FlushLogs();
495
496 std::map<wxString, Session> logs;
497
498 const wxString& logDir = Debug.GetLogDir();
499 wxArrayString a;
500 int nr = wxDir::GetAllFiles(logDir, &a, "*.txt", wxDIR_FILES);
501
502 // PHD2_GuideLog_2017-12-09_044510.txt
503 {
504 wxRegEx re("PHD2_GuideLog_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{6}\\.txt$");
505 for (int i = 0; i < nr; i++)
506 {
507 const wxString& l = a[i];
508 if (!re.Matches(l))
509 continue;
510
511 // omit zero-size guide logs
512 wxStructStat st;
513 int ret = ::wxStat(l, &st);
514 if (ret != 0)
515 continue;
516 if (st.st_size == 0)
517 continue;
518
519 size_t start, len;
520 re.GetMatch(&start, &len, 0);
521
522 wxString timestamp(l, start + 14, 17);
523 auto it = logs.find(timestamp);
524 if (it == logs.end())
525 {
526 Session s;
527 s.timestamp = timestamp;
528 s.start = SessionStart(timestamp);
529 s.has_guide = true;
530 logs[timestamp] = s;
531 }
532 else
533 {
534 it->second.has_guide = true;
535 }
536 }
537 }
538
539 // PHD2_DebugLog_2017-12-09_044510.txt
540 {
541 wxRegEx re("PHD2_DebugLog_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{6}\\.txt$");
542 for (int i = 0; i < nr; i++)
543 {
544 const wxString& l = a[i];
545 if (!re.Matches(l))
546 continue;
547
548 size_t start, len;
549 re.GetMatch(&start, &len, 0);
550 wxString timestamp = l.substr(start + 14, 17);
551 auto it = logs.find(timestamp);
552 if (it == logs.end())
553 {
554 Session s;
555 s.timestamp = timestamp;
556 s.start = SessionStart(timestamp);
557 s.has_debug = true;
558 logs[timestamp] = s;
559 }
560 else
561 {
562 it->second.has_debug = true;
563 }
564 }
565 }
566
567 s_session.clear();
568 s_session_idx.clear();
569 s_grid_row.clear();
570
571 int r = 0;
572 for (auto it = logs.begin(); it != logs.end(); ++it, ++r)
573 {
574 Session& session = it->second;
575 QuickInitSummary(session);
576 s_session.push_back(session);
577 s_session_idx.push_back(r);
578 s_grid_row.push_back(r);
579 }
580
581 // resize grid to hold all sessions (though it may already be large enough)
582 if (grid->GetNumberRows() < s_session.size())
583 grid->AppendRows(s_session.size() - grid->GetNumberRows());
584
585 DoSort(grid); // loads grid
586 }
587
OnIdle(wxIdleEvent & event)588 void LogUploadDialog::OnIdle(wxIdleEvent& event)
589 {
590 bool more = m_scanner.DoWork(100);
591 event.RequestMore(more);
592 }
593
OnIncludeEmpty(wxCommandEvent & ev)594 void LogUploadDialog::OnIncludeEmpty(wxCommandEvent& ev)
595 {
596 s_include_empty = ev.IsChecked();
597 wxGridUpdateLocker noUpdates(m_grid);
598 DoSort(m_grid);
599 }
600
601 struct Uploaded
602 {
603 wxString url;
604 time_t when;
605 };
606 static std::deque<Uploaded> s_recent;
607
LoadRecentUploads()608 static void LoadRecentUploads()
609 {
610 s_recent.clear();
611
612 // url1 timestamp1 ... urlN timestampN
613 wxString s = pConfig->Global.GetString("/log_uploader/recent", wxEmptyString);
614 wxArrayString ary = ::wxStringTokenize(s);
615 for (size_t i = 0; i + 1 < ary.size(); i += 2)
616 {
617 Uploaded t;
618 t.url = ary[i];
619 unsigned long val;
620 if (!ary[i + 1].ToULong(&val))
621 continue;
622 t.when = static_cast<time_t>(val);
623 s_recent.push_back(t);
624 }
625 }
626
SaveUploadInfo(const wxString & url,const wxDateTime & time)627 static void SaveUploadInfo(const wxString& url, const wxDateTime& time)
628 {
629 for (auto it = s_recent.begin(); it != s_recent.end(); ++it)
630 {
631 if (it->url == url)
632 {
633 s_recent.erase(it);
634 break;
635 }
636 }
637 enum { MAX_RECENT = 5 };
638 while (s_recent.size() >= MAX_RECENT)
639 s_recent.pop_front();
640 Uploaded t;
641 t.url = url;
642 t.when = time.GetTicks();
643 s_recent.push_back(t);
644 std::ostringstream os;
645 for (auto it = s_recent.begin(); it != s_recent.end(); ++it)
646 {
647 if (it != s_recent.begin())
648 os << ' ';
649 os << it->url << ' ' << it->when;
650 }
651 pConfig->Global.SetString("/log_uploader/recent", os.str());
652 }
653
654 #define STEP1_TITLE _("Upload Log Files - Select logs")
655 #define STEP2_TITLE _("Upload Log Files - Confirm upload")
656 #define STEP3_TITLE_OK _("Upload Log Files - Upload complete")
657 #define STEP3_TITLE_FAIL _("Upload Log Files - Upload not completed")
658
659 #define STEP1_MESSAGE _( \
660 "When asking for help in the PHD2 Forum it is important to include your PHD2 logs. This tool will " \
661 "help you upload your log files so they can be seen in the forum.\n" \
662 "The first step is to select which files to upload.\n" \
663 )
664
LogUploadDialog(wxWindow * parent)665 LogUploadDialog::LogUploadDialog(wxWindow *parent)
666 :
667 wxDialog(parent, wxID_ANY, STEP1_TITLE, wxDefaultPosition, wxSize(580, 480), wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER),
668 m_step(1),
669 m_nrSelected(0)
670 {
671 SetSizeHints(wxDefaultSize, wxDefaultSize);
672
673 m_text = new wxStaticText(this, wxID_ANY, STEP1_MESSAGE, wxDefaultPosition, wxDefaultSize, 0);
674 m_html = new wxHtmlWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHW_SCROLLBAR_AUTO);
675 m_html->Hide();
676
677 m_grid = new wxGrid(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0);
678
679 // Grid
680 m_grid->CreateGrid(MIN_ROWS, NUM_COLUMNS);
681 m_grid->EnableEditing(false);
682 m_grid->EnableGridLines(true);
683 m_grid->EnableDragGridSize(false);
684 m_grid->SetMargins(0, 0);
685 m_grid->SetSelectionMode(wxGrid::wxGridSelectRows);
686 m_grid->SetDefaultCellAlignment(wxALIGN_CENTRE, wxALIGN_BOTTOM); // doesn't work?
687
688 // Columns
689 m_grid->SetColSize(COL_SELECT, 40);
690 m_grid->SetColSize(COL_NIGHTOF, 200);
691 m_grid->SetColSize(COL_GUIDE, 85);
692 m_grid->SetColSize(COL_CAL, 40);
693 m_grid->SetColSize(COL_GA, 40);
694 m_grid->EnableDragColMove(false);
695 m_grid->EnableDragColSize(true);
696 m_grid->SetColLabelSize(30);
697 m_grid->SetColLabelValue(COL_SELECT, _("Select"));
698 m_grid->SetColLabelValue(COL_NIGHTOF, _("Night Of"));
699 m_grid->SetColLabelValue(COL_GUIDE, _("Guided"));
700 m_grid->SetColLabelValue(COL_CAL, _("Calibration"));
701 m_grid->SetColLabelValue(COL_GA, _("Guide Asst."));
702 m_grid->SetColLabelAlignment(wxALIGN_CENTRE, wxALIGN_CENTRE);
703
704 m_grid->SetSortingColumn(COL_NIGHTOF, false /* descending */);
705 m_grid->UseNativeColHeader(true);
706
707 wxGridCellAttr *attr;
708
709 // log selection
710 attr = new wxGridCellAttr();
711 attr->SetReadOnly(true);
712 m_grid->SetColAttr(COL_SELECT, attr);
713
714 // Rows
715 m_grid->EnableDragRowSize(true);
716 m_grid->SetRowLabelSize(0);
717 m_grid->SetRowLabelAlignment(wxALIGN_CENTRE, wxALIGN_CENTRE);
718
719 m_recent = new wxHyperlinkCtrl(this, wxID_ANY, _("Recent uploads"), wxEmptyString, wxDefaultPosition, wxDefaultSize, wxHL_DEFAULT_STYLE);
720
721 LoadRecentUploads();
722 if (s_recent.empty())
723 m_recent->Hide();
724
725 m_back = new wxButton(this, wxID_ANY, _("< Back"), wxDefaultPosition, wxDefaultSize, 0);
726 m_back->Hide();
727
728 m_upload = new wxButton(this, wxID_ANY, _("Next >"), wxDefaultPosition, wxDefaultSize, 0);
729 m_upload->Enable(false);
730
731 s_include_empty = false;
732 m_includeEmpty = new wxCheckBox(this, wxID_ANY, _("Show logs with no guiding"));
733 m_includeEmpty->SetToolTip(_("Show all available logs, including logs from nights when there was no guiding"));
734
735 wxBoxSizer *sizer0 = new wxBoxSizer(wxVERTICAL); // top-level sizer
736 wxBoxSizer *sizer1 = new wxBoxSizer(wxVERTICAL); // sizer containing the grid
737 wxBoxSizer *sizer2 = new wxBoxSizer(wxHORIZONTAL); // sizer containing the buttons below the grid
738 wxBoxSizer *sizer3 = new wxBoxSizer(wxHORIZONTAL); // sizer containing Recent uploads and Include empty checkbox
739
740 sizer1->Add(m_grid, 0, wxALL | wxEXPAND, 5);
741
742 sizer3->Add(m_recent, 3, wxALL, 5);
743 sizer3->Add(0, 0, 1, wxEXPAND, 5);
744 sizer3->Add(m_includeEmpty, 0, wxALL, 5);
745
746 sizer2->Add(sizer3, 0, wxALL, 5);
747 sizer2->Add(0, 0, 1, wxEXPAND, 5);
748 sizer2->Add(m_back, 0, wxALL | wxALIGN_CENTER_HORIZONTAL, 5);
749 sizer2->Add(m_upload, 0, wxALL | wxALIGN_CENTER_HORIZONTAL, 5);
750
751 sizer0->Add(m_text, 1, wxALL | wxEXPAND, 5);
752 sizer0->Add(m_html, 1, wxALL | wxEXPAND, 5);
753 sizer0->Add(sizer1, 1, wxEXPAND, 5);
754 sizer0->Add(sizer2, 0, wxEXPAND, 5);
755
756 SetSizer(sizer0);
757 Layout();
758
759 Centre(wxBOTH);
760
761 // Connect Events
762 m_recent->Connect(wxEVT_COMMAND_HYPERLINK, wxHyperlinkEventHandler(LogUploadDialog::OnRecentClicked), nullptr, this);
763 m_includeEmpty->Connect(wxEVT_CHECKBOX, wxCommandEventHandler(LogUploadDialog::OnIncludeEmpty), nullptr, this);
764 m_back->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(LogUploadDialog::OnBackClick), nullptr, this);
765 m_upload->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(LogUploadDialog::OnUploadClick), nullptr, this);
766 m_grid->Connect(wxEVT_GRID_CELL_LEFT_CLICK, wxGridEventHandler(LogUploadDialog::OnCellLeftClick), nullptr, this);
767 m_grid->Connect(wxEVT_GRID_COL_SORT, wxGridEventHandler(LogUploadDialog::OnColSort), nullptr, this);
768 m_html->Connect(wxEVT_COMMAND_HTML_LINK_CLICKED, wxHtmlLinkEventHandler(LogUploadDialog::OnLinkClicked), nullptr, this);
769 Connect(wxEVT_IDLE, wxIdleEventHandler(LogUploadDialog::OnIdle), nullptr, this);
770
771 LoadGrid(m_grid);
772
773 m_grid->AutoSizeColumns();
774
775 m_scanner.Init(m_grid);
776 }
777
~LogUploadDialog()778 LogUploadDialog::~LogUploadDialog()
779 {
780 // Disconnect Events
781 m_recent->Disconnect(wxEVT_COMMAND_HYPERLINK, wxHyperlinkEventHandler(LogUploadDialog::OnRecentClicked), nullptr, this);
782 m_includeEmpty->Disconnect(wxEVT_CHECKBOX, wxCommandEventHandler(LogUploadDialog::OnIncludeEmpty), nullptr, this);
783 m_back->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(LogUploadDialog::OnBackClick), nullptr, this);
784 m_upload->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(LogUploadDialog::OnUploadClick), nullptr, this);
785 m_grid->Disconnect(wxEVT_GRID_CELL_LEFT_CLICK, wxGridEventHandler(LogUploadDialog::OnCellLeftClick), nullptr, this);
786 m_grid->Disconnect(wxEVT_GRID_COL_SORT, wxGridEventHandler(LogUploadDialog::OnColSort), nullptr, this);
787 m_html->Disconnect(wxEVT_COMMAND_HTML_LINK_CLICKED, wxHtmlLinkEventHandler(LogUploadDialog::OnLinkClicked), nullptr, this);
788 Disconnect(wxEVT_IDLE, wxIdleEventHandler(LogUploadDialog::OnIdle), nullptr, this);
789 }
790
ToggleCellValue(LogUploadDialog * dlg,int row,int col)791 static void ToggleCellValue(LogUploadDialog *dlg, int row, int col)
792 {
793 wxGrid *grid = dlg->m_grid;
794 bool const newval = grid->GetCellValue(row, col).IsEmpty();
795 grid->SetCellValue(row, col, newval ? "1" : "");
796 if (newval)
797 {
798 if (++dlg->m_nrSelected == 1)
799 dlg->m_upload->Enable(true);
800 }
801 else
802 {
803 if (--dlg->m_nrSelected == 0)
804 dlg->m_upload->Enable(false);
805 }
806 }
807
OnCellLeftClick(wxGridEvent & event)808 void LogUploadDialog::OnCellLeftClick(wxGridEvent& event)
809 {
810 if (event.AltDown() || event.ControlDown() || event.MetaDown() || event.ShiftDown())
811 {
812 event.Skip();
813 return;
814 }
815
816 int r;
817 if ((r = event.GetRow()) < s_session.size() &&
818 event.GetCol() == COL_SELECT)
819 {
820 const Session& session = s_session[s_session_idx[r]];
821 if (session.has_guide || session.has_debug)
822 {
823 ToggleCellValue(this, r, event.GetCol());
824 }
825 }
826
827 event.Skip();
828 }
829
OnColSort(wxGridEvent & event)830 void LogUploadDialog::OnColSort(wxGridEvent& event)
831 {
832 int col = event.GetCol();
833
834 if (col != COL_NIGHTOF)
835 {
836 event.Veto();
837 return;
838 }
839
840 if (m_grid->IsSortingBy(col))
841 m_grid->SetSortingColumn(col, !m_grid->IsSortOrderAscending()); // toggle asc/desc
842 else
843 m_grid->SetSortingColumn(col, true);
844
845 m_grid->BeginBatch();
846
847 DoSort(m_grid);
848
849 m_grid->EndBatch();
850
851 event.Skip();
852 }
853
854 struct AutoChdir
855 {
856 wxString m_prev;
AutoChdirAutoChdir857 AutoChdir(const wxString& dir)
858 {
859 m_prev = wxFileName::GetCwd();
860 wxFileName::SetCwd(dir);
861 }
~AutoChdirAutoChdir862 ~AutoChdir()
863 {
864 wxFileName::SetCwd(m_prev);
865 }
866 };
867
868 struct FileData
869 {
FileDataFileData870 FileData(const wxString& name, const wxDateTime& ts) : filename(name), timestamp(ts) { }
871 wxString filename;
872 wxDateTime timestamp;
873 };
874
875 enum UploadErr
876 {
877 UPL_OK,
878 UPL_INTERNAL_ERROR,
879 UPL_CONNECTION_ERROR,
880 UPL_COMPRESS_ERROR,
881 UPL_SIZE_ERROR,
882 };
883
884 struct BgUpload : public RunInBg
885 {
886 std::vector<FileData> m_input;
887 wxFFile m_ff;
888 CURL *m_curl;
889 std::ostringstream m_response;
890 UploadErr m_err;
891
BgUploadBgUpload892 BgUpload(wxWindow *parent) : RunInBg(parent, _("Upload"), _("Uploading log files ...")), m_curl(nullptr), m_err(UPL_INTERNAL_ERROR) {}
893 ~BgUpload() override;
894 bool Entry() override;
895 };
896
readfn(char * buffer,size_t size,size_t nitems,void * p)897 static size_t readfn(char *buffer, size_t size, size_t nitems, void *p)
898 {
899 BgUpload *upload = static_cast<BgUpload *>(p);
900 return upload->IsCanceled() ? CURL_READFUNC_ABORT : upload->m_ff.Read(buffer, size * nitems);
901 }
902
writefn(char * ptr,size_t size,size_t nmemb,void * p)903 static size_t writefn(char *ptr, size_t size, size_t nmemb, void *p)
904 {
905 BgUpload *upload = static_cast<BgUpload *>(p);
906 size_t len = size * nmemb;
907 upload->m_response.write(ptr, len);
908 return upload->IsCanceled() ? 0 : len;
909 }
910
911 #if defined(OLD_CURL)
progressfn(void * p,double dltotal,double dlnow,double ultotal,double ulnow)912 static int progressfn(void *p, double dltotal, double dlnow, double ultotal, double ulnow)
913 #else
914 static int progressfn(void *p, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow)
915 #endif
916 {
917 BgUpload *upload = static_cast<BgUpload *>(p);
918 if (ultotal)
919 {
920 double pct = (double) ulnow / (double) ultotal * 100.0;
921 upload->SetMessage(wxString::Format(_("Uploading ... %.f%%"), pct));
922 }
923 return upload->IsCanceled() ? 1 : 0;
924 }
925
~BgUpload()926 BgUpload::~BgUpload()
927 {
928 if (m_curl)
929 curl_easy_cleanup(m_curl);
930 }
931
InterruptibleWrite(BgUpload * upload,wxOutputStream & out,wxInputStream & in)932 static bool InterruptibleWrite(BgUpload *upload, wxOutputStream& out, wxInputStream& in)
933 {
934 while (true)
935 {
936 char buf[4096];
937
938 size_t sz = in.Read(buf, WXSIZEOF(buf)).LastRead();
939 if (!sz)
940 return true;
941
942 if (upload->IsCanceled())
943 return false;
944
945 if (out.Write(buf, sz).LastWrite() != sz)
946 {
947 Debug.Write("Upload log: error writing to zip file\n");
948 upload->m_err = UPL_COMPRESS_ERROR;
949 return false;
950 }
951
952 if (upload->IsCanceled())
953 return false;
954 }
955 }
956
AddFile(BgUpload * upload,wxZipOutputStream & zip,const wxString & filename,const wxDateTime & dt)957 static bool AddFile(BgUpload *upload, wxZipOutputStream& zip, const wxString& filename, const wxDateTime& dt)
958 {
959 wxFFileInputStream is(filename);
960 if (!is.IsOk())
961 {
962 Debug.Write(wxString::Format("Upload log: could not open %s\n", filename));
963 upload->m_err = UPL_COMPRESS_ERROR;
964 return false;
965 }
966 zip.PutNextEntry(filename, dt);
967 return InterruptibleWrite(upload, zip, is);
968 }
969
QueryMaxSize(BgUpload * upload)970 static long QueryMaxSize(BgUpload *upload)
971 {
972 curl_easy_setopt(upload->m_curl, CURLOPT_URL, "https://openphdguiding.org/logs/upload?limits");
973
974 upload->SetMessage(_("Connecting ..."));
975
976 int waitSecs[] = { 1, 5, 15, };
977
978 for (int tries = 0; ; tries++)
979 {
980 CURLcode res = curl_easy_perform(upload->m_curl);
981 if (res == CURLE_OK)
982 break;
983
984 if (tries < WXSIZEOF(waitSecs))
985 {
986 int secs = waitSecs[tries];
987 Debug.Write(wxString::Format("Upload log: get limits failed: %s, wait %ds for retry\n", curl_easy_strerror(res), secs));
988 for (int i = secs; i > 0; --i)
989 {
990 upload->SetMessage(wxString::Format(_("Connection failed, will retry in %ds"), i));
991 wxSleep(1);
992 if (upload->IsCanceled())
993 return -1;
994 }
995
996 // reset the server response buffer
997 upload->m_response.clear();
998 upload->m_response.str("");
999 continue;
1000 }
1001
1002 Debug.Write(wxString::Format("Upload log: get limits failed: %s\n", curl_easy_strerror(res)));
1003 upload->m_err = UPL_CONNECTION_ERROR;
1004 return -1;
1005 }
1006
1007 long limit = -1;
1008
1009 JsonParser parser;
1010 if (parser.Parse(upload->m_response.str()))
1011 {
1012 json_for_each(n, parser.Root())
1013 {
1014 if (!n->name)
1015 continue;
1016 if (strcmp(n->name, "max_file_size") == 0 && n->type == JSON_INT)
1017 {
1018 limit = n->int_value;
1019 break;
1020 }
1021 }
1022 }
1023
1024 if (limit == -1)
1025 {
1026 Debug.Write(wxString::Format("Upload log: get limits failed, server response = %s\n", upload->m_response.str()));
1027 upload->m_err = UPL_CONNECTION_ERROR;
1028 }
1029
1030 return limit;
1031 }
1032
Entry()1033 bool BgUpload::Entry()
1034 {
1035 m_curl = curl_easy_init();
1036 if (!m_curl)
1037 {
1038 Debug.Write("Upload log: curl init failed!\n");
1039 m_err = UPL_CONNECTION_ERROR;
1040 return false;
1041 }
1042
1043 curl_easy_setopt(m_curl, CURLOPT_USERAGENT, static_cast<const char *>(wxGetApp().UserAgent().c_str()));
1044
1045 // setup write callback to capture server responses
1046 curl_easy_setopt(m_curl, CURLOPT_WRITEFUNCTION, writefn);
1047 curl_easy_setopt(m_curl, CURLOPT_WRITEDATA, this);
1048
1049 long limit = QueryMaxSize(this);
1050 if (limit == -1)
1051 return false;
1052
1053 const wxString& logDir = Debug.GetLogDir();
1054
1055 AutoChdir cd(logDir);
1056 wxLogNull nolog;
1057
1058 wxString zipfile("PHD2_upload.zip");
1059 ::wxRemove(zipfile);
1060
1061 wxFFileOutputStream out(zipfile);
1062 wxZipOutputStream zip(out);
1063
1064 for (auto it = m_input.begin(); it != m_input.end(); ++it)
1065 {
1066 SetMessage(wxString::Format("Compressing %s...", it->filename));
1067 if (!AddFile(this, zip, it->filename, it->timestamp))
1068 return false;
1069 if (IsCanceled())
1070 return false;
1071 }
1072
1073 zip.Close();
1074 out.Close();
1075
1076 SetMessage("Uploading ...");
1077
1078 Debug.Write(wxString::Format("Upload log file %s\n", zipfile));
1079
1080 m_ff.Open(zipfile, "rb");
1081 if (!m_ff.IsOpened())
1082 {
1083 Debug.Write("Upload log: could not open zip file for reading\n");
1084 m_err = UPL_COMPRESS_ERROR;
1085 return false;
1086 }
1087
1088 // get the file size
1089 m_ff.SeekEnd();
1090 wxFileOffset len = m_ff.Tell();
1091 m_ff.Seek(0);
1092
1093 if (len > limit)
1094 {
1095 Debug.Write(wxString::Format("Upload log: upload size %lu bytes exceeds limit of %ld\n", (unsigned long) len, limit));
1096 m_err = UPL_SIZE_ERROR;
1097 return false;
1098 }
1099
1100 Debug.Write(wxString::Format("Upload log: upload size is %lu bytes\n", len));
1101
1102 // setup for upload
1103
1104 // clear prior response
1105 m_response.clear();
1106 m_response.str("");
1107
1108 curl_easy_setopt(m_curl, CURLOPT_URL, "https://openphdguiding.org/logs/upload");
1109
1110 // enable upload
1111 curl_easy_setopt(m_curl, CURLOPT_UPLOAD, 1L);
1112
1113 // setup read callback
1114 curl_easy_setopt(m_curl, CURLOPT_READFUNCTION, readfn);
1115 curl_easy_setopt(m_curl, CURLOPT_READDATA, this);
1116
1117 // setup progress callback to allow cancelling
1118 #if defined(OLD_CURL)
1119 curl_easy_setopt(m_curl, CURLOPT_PROGRESSFUNCTION, progressfn);
1120 curl_easy_setopt(m_curl, CURLOPT_PROGRESSDATA, this);
1121 #else // modern libcurl
1122 curl_easy_setopt(m_curl, CURLOPT_XFERINFOFUNCTION, progressfn);
1123 curl_easy_setopt(m_curl, CURLOPT_XFERINFODATA, this);
1124 #endif
1125 curl_easy_setopt(m_curl, CURLOPT_NOPROGRESS, 0L);
1126
1127 // give the size of the upload
1128 curl_easy_setopt(m_curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t) len);
1129
1130 // do the upload
1131
1132 int waitSecs[] = { 1, 5, 15, };
1133
1134 for (int tries = 0; ; tries++)
1135 {
1136 CURLcode res = curl_easy_perform(m_curl);
1137 if (res == CURLE_OK)
1138 break;
1139
1140 if (tries < WXSIZEOF(waitSecs))
1141 {
1142 int secs = waitSecs[tries];
1143 Debug.Write(wxString::Format("Upload log: upload failed: %s, wait %ds for retry\n", curl_easy_strerror(res), secs));
1144 for (int i = secs; i > 0; --i)
1145 {
1146 SetMessage(wxString::Format(_("Upload failed, will retry in %ds"), i));
1147 wxSleep(1);
1148 if (IsCanceled())
1149 return false;
1150 }
1151
1152 // rewind the input file and reset the server response buffer
1153 m_ff.Seek(0);
1154 m_response.clear();
1155 m_response.str("");
1156 continue;
1157 }
1158
1159 Debug.Write(wxString::Format("Upload log: upload failed: %s\n", curl_easy_strerror(res)));
1160 m_err = UPL_CONNECTION_ERROR;
1161 return false;
1162 }
1163
1164 // log the transfer info
1165 double speed_upload, total_time;
1166 curl_easy_getinfo(m_curl, CURLINFO_SPEED_UPLOAD, &speed_upload);
1167 curl_easy_getinfo(m_curl, CURLINFO_TOTAL_TIME, &total_time);
1168
1169 Debug.Write(wxString::Format("Upload log: %.3f bytes/sec, %.3f seconds elapsed\n",
1170 speed_upload, total_time));
1171
1172 return true;
1173 }
1174
ConfirmUpload()1175 void LogUploadDialog::ConfirmUpload()
1176 {
1177 m_step = 2;
1178
1179 wxString msg(_("The following log files will be uploaded:") + "<pre>");
1180
1181 for (int r = 0; r < s_session.size(); r++)
1182 {
1183 bool selected = !m_grid->GetCellValue(r, COL_SELECT).IsEmpty();
1184 if (!selected)
1185 continue;
1186
1187 int idx = s_session_idx[r];
1188 const Session& session = s_session[idx];
1189
1190 wxString logs;
1191 if (session.has_guide && session.has_debug)
1192 logs = _("Guide and Debug logs");
1193 else if (session.has_debug)
1194 logs = _("Debug log");
1195 else
1196 logs = _("Guide log");
1197
1198 msg += wxString::Format("%-20s %-27s %s<br>", m_grid->GetCellValue(r, COL_NIGHTOF),
1199 FormatTimestamp(session.start), logs);
1200 }
1201 msg += "</pre>";
1202
1203 WindowUpdateLocker noUpdates(this);
1204 SetTitle(STEP2_TITLE);
1205 m_text->Hide();
1206 m_html->SetPage(msg);
1207 m_html->Show();
1208 m_grid->Hide();
1209 m_recent->Hide();
1210 m_includeEmpty->Hide();
1211 m_back->Show();
1212 m_upload->Show();
1213 m_upload->SetLabel(_("Upload"));
1214 Layout();
1215 }
1216
ExecUpload()1217 void LogUploadDialog::ExecUpload()
1218 {
1219 m_upload->Enable(false);
1220 m_back->Enable(false);
1221
1222 BgUpload upload(this);
1223
1224 for (int r = 0; r < s_session.size(); r++)
1225 {
1226 const Session& session = s_session[s_session_idx[r]];
1227 bool selected = !m_grid->GetCellValue(r, COL_SELECT).IsEmpty();
1228
1229 if (!selected)
1230 continue;
1231
1232 if (session.has_guide)
1233 upload.m_input.push_back(FileData(GuideLogName(session), session.start));
1234 if (session.has_debug)
1235 upload.m_input.push_back(FileData(DebugLogName(session), session.start));
1236 }
1237
1238 upload.SetPopupDelay(500);
1239 bool ok = upload.Run();
1240
1241 m_upload->Enable(true);
1242 m_back->Enable(true);
1243
1244 if (!ok && upload.IsCanceled())
1245 {
1246 // cancelled, do nothing
1247 return;
1248 }
1249
1250 wxString url;
1251 wxString err;
1252
1253 if (ok)
1254 {
1255 std::string s(upload.m_response.str());
1256 Debug.Write(wxString::Format("Upload log: server response: %s\n", s));
1257
1258 JsonParser parser;
1259 if (parser.Parse(s))
1260 {
1261 json_for_each (n, parser.Root())
1262 {
1263 if (!n->name)
1264 continue;
1265 if (strcmp(n->name, "url") == 0 && n->type == JSON_STRING)
1266 url = n->string_value;
1267 else if (strcmp(n->name, "error") == 0 && n->type == JSON_STRING)
1268 err = n->string_value;
1269 }
1270 }
1271
1272 if (url.empty())
1273 {
1274 ok = false;
1275 upload.m_err = UPL_CONNECTION_ERROR;
1276 }
1277 }
1278
1279 if (ok)
1280 {
1281 SaveUploadInfo(url, wxDateTime::Now());
1282 wxString msg = wxString::Format(_("The log files have been uploaded and can be accessed at this link:") + "<br>"
1283 "<br>"
1284 "<font size=-1>%s</font><br><br>" +
1285 _("You can share your log files in the <a href=\"forum\">PHD2 Forum</a> by posting a message that includes the link. "
1286 "Copy and paste the link into your forum post.") + "<br><br>" +
1287 wxString::Format("<a href=\"copy.%u\">", (unsigned int)(s_recent.size() - 1)) + _("Copy link to clipboard"), url);
1288 WindowUpdateLocker noUpdates(this);
1289 SetTitle(STEP3_TITLE_OK);
1290 m_html->SetPage(msg);
1291 m_back->Hide();
1292 m_upload->Hide();
1293 Layout();
1294 return;
1295 }
1296
1297 wxString msg;
1298
1299 switch (upload.m_err)
1300 {
1301 default:
1302 case UPL_INTERNAL_ERROR:
1303 msg = _("PHD2 was unable to upload the log files due to an internal error. Please report the error in the PHD2 Forum.");
1304 break;
1305 case UPL_CONNECTION_ERROR:
1306 msg = _("PHD2 was unable to upload the log files. The service may be temproarily unavailable. "
1307 "You can try again later or you can try another file sharing service such as Dropbox or Google Drive.");
1308 break;
1309 case UPL_COMPRESS_ERROR:
1310 msg = _("PHD2 encountered an error while compressing the log files. Please make sure the selected logs are "
1311 "available and try again.");
1312 break;
1313 case UPL_SIZE_ERROR:
1314 msg = _("The total compressed size of the selected log files exceeds the maximum size allowed. Try selecting "
1315 "fewer files, or use an alternative file sharing service such as Dropbox or Google Drive.");
1316 break;
1317 }
1318
1319 WindowUpdateLocker noUpdates(this);
1320 SetTitle(STEP3_TITLE_FAIL);
1321 m_html->SetPage(msg);
1322 m_back->Show();
1323 m_upload->Hide();
1324 m_step = 3;
1325 Layout();
1326 }
1327
OnRecentClicked(wxHyperlinkEvent & event)1328 void LogUploadDialog::OnRecentClicked(wxHyperlinkEvent& event)
1329 {
1330 std::ostringstream os;
1331 os << "<table><tr><th>Uploaded</th><th>Link</th><th> </th></tr>";
1332 int i = s_recent.size() - 1;
1333 for (auto it = s_recent.rbegin(); it != s_recent.rend(); ++it, --i)
1334 {
1335 os << "<tr><td>" << wxDateTime(it->when).Format() << "</td>"
1336 << "<td><font size=-1>" << it->url << "</font></td>"
1337 << "<td><a href=\"copy." << i << "\">Copy link</a></td></tr>";
1338 }
1339 os << "</table>";
1340
1341 WindowUpdateLocker noUpdates(this);
1342 SetTitle(_("Recent uploads"));
1343 m_text->Hide();
1344 m_grid->Hide();
1345 m_html->SetPage(os.str());
1346 m_html->Show();
1347 m_recent->Hide();
1348 m_includeEmpty->Hide();
1349 m_upload->Hide();
1350 Layout();
1351 }
1352
OnBackClick(wxCommandEvent & event)1353 void LogUploadDialog::OnBackClick(wxCommandEvent& event)
1354 {
1355 if (m_step == 2)
1356 {
1357 WindowUpdateLocker noUpdates(this);
1358 m_step = 1;
1359 SetTitle(STEP1_TITLE);
1360 m_text->Show();
1361 m_html->Hide();
1362 m_grid->Show();
1363 m_recent->Show(!s_recent.empty());
1364 m_includeEmpty->Show();
1365 m_back->Hide();
1366 m_upload->SetLabel(_("Next >"));
1367 Layout();
1368 }
1369 else // step 3
1370 {
1371 ConfirmUpload();
1372 }
1373 }
1374
OnUploadClick(wxCommandEvent & event)1375 void LogUploadDialog::OnUploadClick(wxCommandEvent& event)
1376 {
1377 if (m_step == 1)
1378 ConfirmUpload();
1379 else
1380 ExecUpload();
1381 }
1382
OnLinkClicked(wxHtmlLinkEvent & event)1383 void LogUploadDialog::OnLinkClicked(wxHtmlLinkEvent& event)
1384 {
1385 wxString href = event.GetLinkInfo().GetHref();
1386 if (href.StartsWith("copy."))
1387 {
1388 unsigned long val;
1389 if (!href.substr(5).ToULong(&val) || val >= s_recent.size())
1390 return;
1391 if (wxTheClipboard->Open())
1392 {
1393 wxTheClipboard->SetData(new wxURLDataObject(s_recent[val].url));
1394 wxTheClipboard->Close();
1395 }
1396
1397 wxRichToolTip tip(_("Link copied to clipboard"), wxEmptyString);
1398 tip.SetTipKind(wxTipKind_None);
1399 tip.SetBackgroundColour(wxColor(0xe5, 0xdc, 0x62));
1400 tip.ShowFor(m_html);
1401 }
1402 else if (href == "forum")
1403 {
1404 wxLaunchDefaultBrowser("https://groups.google.com/forum/?fromgroups=#!forum/open-phd-guiding");
1405 }
1406 }
1407
UploadLogs()1408 void LogUploader::UploadLogs()
1409 {
1410 LogUploadDialog(pFrame).ShowModal();
1411 }
1412