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>&nbsp;</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