/* */ // // Multi-threaded GUI program example for libaria2. The downloads can // be added using Download -> Add URI menu. The progress is shown in // the main window. // // Compile and link like this: // $ g++ -O2 -Wall -g -std=c++11 `wx-config --cflags` -o libaria2wx // libaria2wx.cc `wx-config --libs` -laria2 -pthread #include #include #include #include #include #include #include // Interface to send message to downloader thread from UI thread struct Job { virtual ~Job(){}; virtual void execute(aria2::Session* session) = 0; }; class MainFrame; // Interface to report back to UI thread from downloader thread struct Notification { virtual ~Notification(){}; virtual void notify(MainFrame* frame) = 0; }; // std::queue wrapper synchronized by mutex. In this example // program, only one thread consumes from the queue, so separating // empty() and pop() is not a problem. template class SynchronizedQueue { public: SynchronizedQueue() {} ~SynchronizedQueue() {} void push(std::unique_ptr&& t) { std::lock_guard l(m_); q_.push(std::move(t)); } std::unique_ptr pop() { std::lock_guard l(m_); std::unique_ptr t = std::move(q_.front()); q_.pop(); return t; } bool empty() { std::lock_guard l(m_); return q_.empty(); } private: std::queue> q_; std::mutex m_; }; typedef SynchronizedQueue JobQueue; typedef SynchronizedQueue NotifyQueue; // Job to shutdown downloader thread struct ShutdownJob : public Job { ShutdownJob(bool force) : force(force) {} virtual void execute(aria2::Session* session) { aria2::shutdown(session, force); } bool force; }; // Job to send URI to download and options to downloader thread struct AddUriJob : public Job { AddUriJob(std::vector&& uris, aria2::KeyVals&& options) : uris(uris), options(options) { } virtual void execute(aria2::Session* session) { // TODO check return value aria2::addUri(session, 0, uris, options); } std::vector uris; aria2::KeyVals options; }; int downloaderJob(JobQueue& jobq, NotifyQueue& notifyq); // This struct is used to report download progress for active // downloads from downloader thread to UI thread. struct DownloadStatus { aria2::A2Gid gid; int64_t totalLength; int64_t completedLength; int downloadSpeed; int uploadSpeed; std::string filename; }; class Aria2App : public wxApp { public: virtual bool OnInit(); virtual int OnExit(); }; class MainFrame : public wxFrame { public: MainFrame(const wxString& title); void OnQuit(wxCommandEvent& event); void OnAbout(wxCommandEvent& event); void OnCloseWindow(wxCloseEvent& event); void OnTimer(wxTimerEvent& event); void OnAddUri(wxCommandEvent& event); void UpdateActiveStatus(const std::vector& v); private: wxTextCtrl* text_; wxTimer timer_; JobQueue jobq_; NotifyQueue notifyq_; std::thread downloaderThread_; DECLARE_EVENT_TABLE() }; enum { TIMER_ID = 1 }; enum { MI_ADD_URI = 1 }; BEGIN_EVENT_TABLE(MainFrame, wxFrame) EVT_CLOSE(MainFrame::OnCloseWindow) EVT_TIMER(TIMER_ID, MainFrame::OnTimer) EVT_MENU(MI_ADD_URI, MainFrame::OnAddUri) END_EVENT_TABLE() class AddUriDialog : public wxDialog { public: AddUriDialog(wxWindow* parent); void OnButton(wxCommandEvent& event); wxString GetUri(); wxString GetOption(); private: wxTextCtrl* uriText_; wxTextCtrl* optionText_; wxButton* okBtn_; wxButton* cancelBtn_; DECLARE_EVENT_TABLE() }; BEGIN_EVENT_TABLE(AddUriDialog, wxDialog) EVT_BUTTON(wxID_ANY, AddUriDialog::OnButton) END_EVENT_TABLE() IMPLEMENT_APP(Aria2App) bool Aria2App::OnInit() { if (!wxApp::OnInit()) return false; aria2::libraryInit(); MainFrame* frame = new MainFrame(wxT("libaria2 GUI example")); frame->Show(true); return true; } int Aria2App::OnExit() { aria2::libraryDeinit(); return wxApp::OnExit(); } MainFrame::MainFrame(const wxString& title) : wxFrame(nullptr, wxID_ANY, title, wxDefaultPosition, wxSize(640, 400)), timer_(this, TIMER_ID), downloaderThread_(downloaderJob, std::ref(jobq_), std::ref(notifyq_)) { wxMenu* downloadMenu = new wxMenu; downloadMenu->Append(MI_ADD_URI, wxT("&Add URI"), wxT("Add URI to download")); wxMenuBar* menuBar = new wxMenuBar(); menuBar->Append(downloadMenu, wxT("&Download")); SetMenuBar(menuBar); // Show active downloads in textual manner wxPanel* panel = new wxPanel(this, wxID_ANY); wxBoxSizer* box = new wxBoxSizer(wxVERTICAL); box->Add(new wxStaticText(panel, wxID_ANY, wxT("Active Download(s)"))); text_ = new wxTextCtrl(panel, wxID_ANY, wxT(""), wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE | wxTE_READONLY); box->Add(text_, wxSizerFlags().Expand().Proportion(1)); panel->SetSizer(box); // Finally start time here timer_.Start(900); } void MainFrame::OnAddUri(wxCommandEvent& WXUNUSED(event)) { AddUriDialog dlg(this); int ret = dlg.ShowModal(); if (ret == 0) { if (dlg.GetUri().IsEmpty()) { return; } std::vector uris = {std::string(dlg.GetUri().mb_str())}; std::string optstr(dlg.GetOption().mb_str()); aria2::KeyVals options; int keyfirst = 0; for (int i = 0; i < (int)optstr.size(); ++i) { if (optstr[i] == '\n') { keyfirst = i + 1; } else if (optstr[i] == '=') { int j; for (j = i + 1; j < (int)optstr.size(); ++j) { if (optstr[j] == '\n') { break; } } if (i - keyfirst > 0) { options.push_back( std::make_pair(optstr.substr(keyfirst, i - keyfirst), optstr.substr(i + 1, j - i - 1))); } keyfirst = j + 1; i = j; } } jobq_.push(std::unique_ptr( new AddUriJob(std::move(uris), std::move(options)))); } } void MainFrame::OnCloseWindow(wxCloseEvent& WXUNUSED(event)) { // On exit, we have to shutdown downloader thread and wait for it to // join. This is needed to execute graceful shutdown sequence of // aria2 session. jobq_.push(std::unique_ptr(new ShutdownJob(true))); downloaderThread_.join(); Destroy(); } void MainFrame::OnTimer(wxTimerEvent& event) { while (!notifyq_.empty()) { std::unique_ptr nt = notifyq_.pop(); nt->notify(this); } } template std::string abbrevsize(T size) { if (size >= 1024 * 1024 * 1024) { return std::to_string(size / 1024 / 1024 / 1024) + "G"; } else if (size >= 1024 * 1024) { return std::to_string(size / 1024 / 1024) + "M"; } else if (size >= 1024) { return std::to_string(size / 1024) + "K"; } else { return std::to_string(size); } } wxString towxs(const std::string& s) { return wxString(s.c_str(), wxConvUTF8); } void MainFrame::UpdateActiveStatus(const std::vector& v) { text_->Clear(); for (auto& a : v) { *text_ << wxT("[") << towxs(aria2::gidToHex(a.gid)) << wxT("] ") << towxs(abbrevsize(a.completedLength)) << wxT("/") << towxs(abbrevsize(a.totalLength)) << wxT("(") << (a.totalLength != 0 ? a.completedLength * 100 / a.totalLength : 0) << wxT("%)") << wxT(" D:") << towxs(abbrevsize(a.downloadSpeed)) << wxT(" U:") << towxs(abbrevsize(a.uploadSpeed)) << wxT("\n") << wxT("File:") << towxs(a.filename) << wxT("\n"); } } AddUriDialog::AddUriDialog(wxWindow* parent) : wxDialog(parent, wxID_ANY, wxT("Add URI"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) { wxPanel* panel = new wxPanel(this, wxID_ANY); wxBoxSizer* box = new wxBoxSizer(wxVERTICAL); // URI text input box->Add(new wxStaticText(panel, wxID_ANY, wxT("URI"))); uriText_ = new wxTextCtrl(panel, wxID_ANY); box->Add(uriText_, wxSizerFlags().Align(wxGROW)); // Option multi text input box->Add(new wxStaticText( panel, wxID_ANY, wxT("Options (key=value pair per line, e.g. dir=/tmp"))); optionText_ = new wxTextCtrl(panel, wxID_ANY, wxT(""), wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE); box->Add(optionText_, wxSizerFlags().Align(wxGROW)); // buttons wxPanel* btnpanel = new wxPanel(panel, wxID_ANY); box->Add(btnpanel); wxBoxSizer* btnbox = new wxBoxSizer(wxHORIZONTAL); // OK button okBtn_ = new wxButton(btnpanel, wxID_ANY, wxT("OK")); btnbox->Add(okBtn_); // Cancel button cancelBtn_ = new wxButton(btnpanel, wxID_ANY, wxT("Cancel")); btnbox->Add(cancelBtn_); panel->SetSizer(box); btnpanel->SetSizer(btnbox); } void AddUriDialog::OnButton(wxCommandEvent& event) { int ret = -1; if (event.GetEventObject() == okBtn_) { ret = 0; } EndModal(ret); } wxString AddUriDialog::GetUri() { return uriText_->GetValue(); } wxString AddUriDialog::GetOption() { return optionText_->GetValue(); } struct DownloadStatusNotification : public Notification { DownloadStatusNotification(std::vector&& v) : v(v) {} virtual void notify(MainFrame* frame) { frame->UpdateActiveStatus(v); } std::vector v; }; struct ShutdownNotification : public Notification { ShutdownNotification() {} virtual void notify(MainFrame* frame) { frame->Close(); } }; int downloaderJob(JobQueue& jobq, NotifyQueue& notifyq) { // session is actually singleton: 1 session per process aria2::Session* session; // Use default configuration aria2::SessionConfig config; config.keepRunning = true; session = aria2::sessionNew(aria2::KeyVals(), config); auto start = std::chrono::steady_clock::now(); for (;;) { int rv = aria2::run(session, aria2::RUN_ONCE); if (rv != 1) { break; } auto now = std::chrono::steady_clock::now(); auto count = std::chrono::duration_cast(now - start) .count(); while (!jobq.empty()) { std::unique_ptr job = jobq.pop(); job->execute(session); } if (count >= 900) { start = now; std::vector gids = aria2::getActiveDownload(session); std::vector v; for (auto gid : gids) { aria2::DownloadHandle* dh = aria2::getDownloadHandle(session, gid); if (dh) { DownloadStatus st; st.gid = gid; st.totalLength = dh->getTotalLength(); st.completedLength = dh->getCompletedLength(); st.downloadSpeed = dh->getDownloadSpeed(); st.uploadSpeed = dh->getUploadSpeed(); if (dh->getNumFiles() > 0) { aria2::FileData file = dh->getFile(1); st.filename = file.path; } v.push_back(std::move(st)); aria2::deleteDownloadHandle(dh); } } notifyq.push(std::unique_ptr( new DownloadStatusNotification(std::move(v)))); } } int rv = aria2::sessionFinal(session); // Report back to the UI thread that this thread is going to // exit. This is needed when user pressed ctrl-C in the terminal. notifyq.push(std::unique_ptr(new ShutdownNotification())); return rv; }