Prechádzať zdrojové kódy

Add libaria2 multi-threaded GUI examle program using wx

Tatsuhiro Tsujikawa 12 rokov pred
rodič
commit
1f38699d32
1 zmenil súbory, kde vykonal 453 pridanie a 0 odobranie
  1. 453 0
      examples/libaria2wx.cc

+ 453 - 0
examples/libaria2wx.cc

@@ -0,0 +1,453 @@
+/* <!-- copyright */
+/*
+ * aria2 - The high speed download utility
+ *
+ * Copyright (C) 2013 Tatsuhiro Tsujikawa
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL.  If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so.  If you
+ * do not wish to do so, delete this exception statement from your
+ * version.  If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ */
+/* copyright --> */
+//
+// 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 <iostream>
+#include <chrono>
+#include <thread>
+#include <mutex>
+#include <queue>
+
+#include <wx/wx.h>
+
+#include <aria2/aria2.h>
+
+// 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<T> 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<typename T>
+class SynchronizedQueue {
+public:
+  SynchronizedQueue() {}
+  ~SynchronizedQueue() {}
+  void push(std::unique_ptr<T>&& t)
+  {
+    std::lock_guard<std::mutex> l(m_);
+    q_.push(std::move(t));
+  }
+  std::unique_ptr<T> pop()
+  {
+    std::lock_guard<std::mutex> l(m_);
+    std::unique_ptr<T> t = std::move(q_.front());
+    q_.pop();
+    return t;
+  }
+  bool empty()
+  {
+    std::lock_guard<std::mutex> l(m_);
+    return q_.empty();
+  }
+private:
+  std::queue<std::unique_ptr<T> > q_;
+  std::mutex m_;
+};
+
+typedef SynchronizedQueue<Job> JobQueue;
+typedef SynchronizedQueue<Notification> 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<std::string>&& uris, aria2::KeyVals&& options)
+    : uris(uris), options(options)
+  {}
+  virtual void execute(aria2::Session* session)
+  {
+    aria2::A2Gid gid;
+    // TODO check return value
+    aria2::addUri(session, gid, uris, options);
+  }
+  std::vector<std::string> 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<DownloadStatus>& 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<std::string> 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<Job>(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<Job>(new ShutdownJob(true)));
+  downloaderThread_.join();
+  Destroy();
+}
+
+void MainFrame::OnTimer(wxTimerEvent& event)
+{
+  while(!notifyq_.empty()) {
+    std::unique_ptr<Notification> nt = notifyq_.pop();
+    nt->notify(this);
+  }
+}
+
+template<typename T>
+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<DownloadStatus>& 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<DownloadStatus>&& v)
+    : v(v) {}
+  virtual void notify(MainFrame* frame)
+  {
+    frame->UpdateActiveStatus(v);
+  }
+  std::vector<DownloadStatus> 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<std::chrono::milliseconds>
+      (now - start).count();
+    while(!jobq.empty()) {
+      std::unique_ptr<Job> job = jobq.pop();
+      job->execute(session);
+    }
+    if(count >= 900) {
+      start = now;
+      std::vector<aria2::A2Gid> gids = aria2::getActiveDownload(session);
+      std::vector<DownloadStatus> 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();
+          std::vector<aria2::FileData> files = dh->getFiles();
+          if(!files.empty()) {
+            st.filename = files[0].path;
+          }
+          v.push_back(std::move(st));
+          aria2::deleteDownloadHandle(dh);
+        }
+      }
+      notifyq.push(std::unique_ptr<Notification>
+                   (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<Notification>(new ShutdownNotification()));
+  return rv;
+}
+