Some initial code to get Downloader non-blocking
[supertux.git] / src / addon / downloader.cpp
1 //  SuperTux
2 //  Copyright (C) 2007 Christoph Sommer <christoph.sommer@2007.expires.deltadevelopment.de>
3 //                2014 Ingo Ruhnke <grumbel@gmail.com>
4 //
5 //  This program is free software: you can redistribute it and/or modify
6 //  it under the terms of the GNU General Public License as published by
7 //  the Free Software Foundation, either version 3 of the License, or
8 //  (at your option) any later version.
9 //
10 //  This program is distributed in the hope that it will be useful,
11 //  but WITHOUT ANY WARRANTY; without even the implied warranty of
12 //  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 //  GNU General Public License for more details.
14 //
15 //  You should have received a copy of the GNU General Public License
16 //  along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18 #include "addon/downloader.hpp"
19
20 #include <algorithm>
21 #include <memory>
22 #include <physfs.h>
23 #include <stdexcept>
24
25 #include "util/log.hpp"
26 #include "version.h"
27
28 namespace {
29
30 size_t my_curl_string_append(void* ptr, size_t size, size_t nmemb, void* userdata)
31 {
32   std::string& s = *static_cast<std::string*>(userdata);
33   std::string buf(static_cast<char*>(ptr), size * nmemb);
34   s += buf;
35   log_debug << "read " << size * nmemb << " bytes of data..." << std::endl;
36   return size * nmemb;
37 }
38
39 size_t my_curl_physfs_write(void* ptr, size_t size, size_t nmemb, void* userdata)
40 {
41   PHYSFS_file* f = static_cast<PHYSFS_file*>(userdata);
42   PHYSFS_sint64 written = PHYSFS_write(f, ptr, size, nmemb);
43   log_debug << "read " << size * nmemb << " bytes of data..." << std::endl;
44   return size * written;
45 }
46
47 } // namespace
48
49 class cURLTransfer : public Transfer
50 {
51 private:
52   Downloader& m_downloader;
53
54   std::string m_url;
55   CURL* m_handle;
56   std::array<char, CURL_ERROR_SIZE> m_error_buffer;
57
58   curl_off_t m_dltotal;
59   curl_off_t m_dlnow;
60   curl_off_t m_ultotal;
61   curl_off_t m_ulnow;
62
63 public:
64   cURLTransfer(Downloader& downloader, const std::string& url) :
65     m_downloader(downloader),
66     m_url(url),
67     m_handle(curl_easy_init()),
68     m_error_buffer(),
69     m_dltotal(0),
70     m_dlnow(0),
71     m_ultotal(0),
72     m_ulnow(0)
73   {
74     if (!m_handle)
75     {
76       throw std::runtime_error("curl_easy_init() failed");
77     }
78     else
79     {
80       curl_easy_setopt(m_handle, CURLOPT_URL, url.c_str());
81       curl_easy_setopt(m_handle, CURLOPT_USERAGENT, "SuperTux/" PACKAGE_VERSION " libcURL");
82
83       curl_easy_setopt(m_handle, CURLOPT_WRITEDATA, this);
84       curl_easy_setopt(m_handle, CURLOPT_WRITEFUNCTION,
85                        [](void* ptr, size_t size, size_t nmemb, void* userdata) -> size_t
86                        {
87                          return static_cast<cURLTransfer*>(userdata)
88                            ->on_data(ptr, size, nmemb);
89                        });
90
91       curl_easy_setopt(m_handle, CURLOPT_ERRORBUFFER, m_error_buffer.data());
92       curl_easy_setopt(m_handle, CURLOPT_NOSIGNAL, 1);
93       curl_easy_setopt(m_handle, CURLOPT_FAILONERROR, 1);
94       curl_easy_setopt(m_handle, CURLOPT_FOLLOWLOCATION, 1);
95
96       curl_easy_setopt(m_handle, CURLOPT_NOPROGRESS, 0);
97       curl_easy_setopt(m_handle, CURLOPT_XFERINFODATA, this);
98       curl_easy_setopt(m_handle, CURLOPT_XFERINFOFUNCTION,
99                        [](void* userdata,
100                           curl_off_t dltotal, curl_off_t dlnow,
101                           curl_off_t ultotal, curl_off_t ulnow)
102                        {
103                          return static_cast<cURLTransfer*>(userdata)
104                            ->on_progress(dltotal, dlnow,
105                                          ultotal, ulnow);
106                        });
107     }
108   }
109
110   ~cURLTransfer()
111   {
112     curl_easy_cleanup(m_handle);
113   }
114
115   CURL* get_curl_handle() const
116   {
117     return m_handle;
118   }
119
120   std::string get_url() const
121   {
122     return m_url;
123   }
124
125   size_t on_data(void* ptr, size_t size, size_t nmemb)
126   {
127     return 0;
128   }
129
130   void on_progress(curl_off_t dltotal, curl_off_t dlnow,
131                    curl_off_t ultotal, curl_off_t ulnow)
132   {
133     m_dltotal = dltotal;
134     m_dlnow = dlnow;
135
136     m_ultotal = ultotal;
137     m_ulnow = ulnow;
138   }
139
140   void abort()
141   {
142     m_downloader.abort(*this);
143   }
144
145 private:
146   cURLTransfer(const cURLTransfer&) = delete;
147   cURLTransfer& operator=(const cURLTransfer&) = delete;
148 };
149
150 Downloader::Downloader() :
151   m_multi_handle(),
152   m_transfers()
153 {
154   curl_global_init(CURL_GLOBAL_ALL);
155   m_multi_handle = curl_multi_init();
156   if (!m_multi_handle)
157   {
158     throw std::runtime_error("curl_multi_init() failed");
159   }
160 }
161
162 Downloader::~Downloader()
163 {
164   for(auto& transfer : m_transfers)
165   {
166     curl_multi_remove_handle(m_multi_handle, transfer->get_curl_handle());
167   }
168   m_transfers.clear();
169
170   curl_multi_cleanup(m_multi_handle);
171   curl_global_cleanup();
172 }
173
174 void
175 Downloader::download(const std::string& url,
176                      size_t (*write_func)(void* ptr, size_t size, size_t nmemb, void* userdata),
177                      void* userdata)
178 {
179   log_info << "Downloading " << url << std::endl;
180
181   char error_buffer[CURL_ERROR_SIZE+1];
182
183   CURL* curl_handle = curl_easy_init();
184   curl_easy_setopt(curl_handle, CURLOPT_URL, url.c_str());
185   curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "SuperTux/" PACKAGE_VERSION " libcURL");
186   curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, write_func);
187   curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, userdata);
188   curl_easy_setopt(curl_handle, CURLOPT_ERRORBUFFER, error_buffer);
189   curl_easy_setopt(curl_handle, CURLOPT_NOPROGRESS, 1);
190   curl_easy_setopt(curl_handle, CURLOPT_NOSIGNAL, 1);
191   curl_easy_setopt(curl_handle, CURLOPT_FAILONERROR, 1);
192   curl_easy_setopt(curl_handle, CURLOPT_FOLLOWLOCATION, 1);
193   CURLcode result = curl_easy_perform(curl_handle);
194   curl_easy_cleanup(curl_handle);
195
196   if (result != CURLE_OK)
197   {
198     std::string why = error_buffer[0] ? error_buffer : "unhandled error";
199     throw std::runtime_error(url + ": download failed: " + why);
200   }
201 }
202
203 std::string
204 Downloader::download(const std::string& url)
205 {
206   std::string result;
207   download(url, my_curl_string_append, &result);
208   return result;
209 }
210
211 void
212 Downloader::download(const std::string& url, const std::string& filename)
213 {
214   std::unique_ptr<PHYSFS_file, int(*)(PHYSFS_File*)> fout(PHYSFS_openWrite(filename.c_str()),
215                                                           PHYSFS_close);
216   download(url, my_curl_physfs_write, fout.get());
217 }
218
219 void
220 Downloader::abort(const cURLTransfer& transfer)
221 {
222   auto it = std::find_if(m_transfers.begin(), m_transfers.end(),
223                          [&transfer](const std::unique_ptr<cURLTransfer>& rhs)
224                          {
225                            return transfer.get_curl_handle() == rhs->get_curl_handle();
226                          });
227   if (it == m_transfers.end())
228   {
229     log_warning << "transfer not found: " << transfer.get_url() << std::endl;
230   }
231   else
232   {
233     curl_multi_remove_handle(m_multi_handle, (*it)->get_curl_handle());
234     m_transfers.erase(it);
235   }
236 }
237
238 void
239 Downloader::update()
240 {
241   // read data from the network
242   CURLMcode ret;
243   int running_handles;
244   while((ret = curl_multi_perform(m_multi_handle, &running_handles)) == CURLM_CALL_MULTI_PERFORM)
245   {}
246
247   // check if any downloads got finished
248   int msgs_in_queue;
249   CURLMsg* msg;
250   while ((msg = curl_multi_info_read(m_multi_handle, &msgs_in_queue)))
251   {
252     switch(msg->msg)
253     {
254       case CURLMSG_DONE:
255         curl_multi_remove_handle(m_multi_handle, msg->easy_handle);
256         //FIXME: finish_transfer(msg->easy_handle);
257         break;
258
259       default:
260         log_warning << "unhandled cURL message: " << msg->msg << std::endl;
261         break;
262     }
263   }
264 }
265
266 TransferHandle
267 Downloader::request_download(const std::string& url, const std::string& filename)
268 {
269   std::unique_ptr<cURLTransfer> transfer(new cURLTransfer(*this, url));
270   curl_multi_add_handle(m_multi_handle, transfer->get_curl_handle());
271   m_transfers.push_back(std::move(transfer));
272   return m_transfers.back().get();
273 }
274
275 /* EOF */