Updated addon repository URL and improved debug output on download
[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 <array>
22 #include <assert.h>
23 #include <memory>
24 #include <physfs.h>
25 #include <sstream>
26 #include <stdexcept>
27
28 #include "util/log.hpp"
29 #include "version.h"
30
31 namespace {
32
33 size_t my_curl_string_append(void* ptr, size_t size, size_t nmemb, void* userdata)
34 {
35   std::string& s = *static_cast<std::string*>(userdata);
36   std::string buf(static_cast<char*>(ptr), size * nmemb);
37   s += buf;
38   log_debug << "read " << size * nmemb << " bytes of data..." << std::endl;
39   return size * nmemb;
40 }
41
42 size_t my_curl_physfs_write(void* ptr, size_t size, size_t nmemb, void* userdata)
43 {
44   PHYSFS_file* f = static_cast<PHYSFS_file*>(userdata);
45   PHYSFS_sint64 written = PHYSFS_write(f, ptr, size, nmemb);
46   log_debug << "read " << size * nmemb << " bytes of data..." << std::endl;
47   return size * written;
48 }
49
50 } // namespace
51
52 void
53 TransferStatus::abort()
54 {
55   m_downloader.abort(id);
56 }
57
58 void
59 TransferStatus::update()
60 {
61   m_downloader.update();
62 }
63
64 class Transfer
65 {
66 private:
67   Downloader& m_downloader;
68   TransferId m_id;
69
70   std::string m_url;
71   CURL* m_handle;
72   std::array<char, CURL_ERROR_SIZE> m_error_buffer;
73
74   TransferStatusPtr m_status;
75   std::unique_ptr<PHYSFS_file, int(*)(PHYSFS_File*)> m_fout;
76
77 public:
78   Transfer(Downloader& downloader, TransferId id,
79            const std::string& url,
80            const std::string& outfile) :
81     m_downloader(downloader),
82     m_id(id),
83     m_url(url),
84     m_handle(),
85     m_error_buffer({{'\0'}}),
86     m_status(new TransferStatus(m_downloader, id)),
87     m_fout(PHYSFS_openWrite(outfile.c_str()), PHYSFS_close)
88   {
89     if (!m_fout)
90     {
91       std::ostringstream out;
92       out << "PHYSFS_openRead() failed: " << PHYSFS_getLastError();
93       throw std::runtime_error(out.str());
94     }
95
96     m_handle = curl_easy_init();
97     if (!m_handle)
98     {
99       throw std::runtime_error("curl_easy_init() failed");
100     }
101     else
102     {
103       curl_easy_setopt(m_handle, CURLOPT_URL, url.c_str());
104       curl_easy_setopt(m_handle, CURLOPT_USERAGENT, "SuperTux/" PACKAGE_VERSION " libcURL");
105
106       curl_easy_setopt(m_handle, CURLOPT_WRITEDATA, this);
107       curl_easy_setopt(m_handle, CURLOPT_WRITEFUNCTION, &Transfer::on_data_wrap);
108
109       curl_easy_setopt(m_handle, CURLOPT_ERRORBUFFER, m_error_buffer.data());
110       curl_easy_setopt(m_handle, CURLOPT_NOSIGNAL, 1);
111       curl_easy_setopt(m_handle, CURLOPT_FAILONERROR, 1);
112       curl_easy_setopt(m_handle, CURLOPT_FOLLOWLOCATION, 1);
113
114       curl_easy_setopt(m_handle, CURLOPT_NOPROGRESS, 0);
115       curl_easy_setopt(m_handle, CURLOPT_PROGRESSDATA, this);
116       curl_easy_setopt(m_handle, CURLOPT_PROGRESSFUNCTION, &Transfer::on_progress_wrap);
117     }
118   }
119
120   ~Transfer()
121   {
122     curl_easy_cleanup(m_handle);
123   }
124
125   TransferStatusPtr get_status() const
126   {
127     return m_status;
128   }
129
130   const char* get_error_buffer() const
131   {
132     return m_error_buffer.data();
133   }
134
135   TransferId get_id() const
136   {
137     return m_id;
138   }
139
140   CURL* get_curl_handle() const
141   {
142     return m_handle;
143   }
144
145   std::string get_url() const
146   {
147     return m_url;
148   }
149
150   size_t on_data(void* ptr, size_t size, size_t nmemb)
151   {
152     PHYSFS_write(m_fout.get(), ptr, size, nmemb);
153     return size * nmemb;
154   }
155
156   int on_progress(double dltotal, double dlnow,
157                    double ultotal, double ulnow)
158   {
159     m_status->dltotal = static_cast<int>(dltotal);
160     m_status->dlnow = static_cast<int>(dlnow);
161
162     m_status->ultotal = static_cast<int>(ultotal);
163     m_status->ulnow = static_cast<int>(ulnow);
164
165     return 0;
166   }
167
168 private:
169   static size_t on_data_wrap(char* ptr, size_t size, size_t nmemb, void* userdata)
170   {
171     return static_cast<Transfer*>(userdata)->on_data(ptr, size, nmemb);
172   }
173
174   static int on_progress_wrap(void* userdata,
175                               double dltotal, double dlnow,
176                               double ultotal, double ulnow)
177   {
178     return static_cast<Transfer*>(userdata)->on_progress(dltotal, dlnow, ultotal, ulnow);
179   }
180
181 private:
182   Transfer(const Transfer&) = delete;
183   Transfer& operator=(const Transfer&) = delete;
184 };
185
186 Downloader::Downloader() :
187   m_multi_handle(),
188   m_transfers(),
189   m_next_transfer_id(1)
190 {
191   curl_global_init(CURL_GLOBAL_ALL);
192   m_multi_handle = curl_multi_init();
193   if (!m_multi_handle)
194   {
195     throw std::runtime_error("curl_multi_init() failed");
196   }
197 }
198
199 Downloader::~Downloader()
200 {
201   for(auto& transfer : m_transfers)
202   {
203     curl_multi_remove_handle(m_multi_handle, transfer->get_curl_handle());
204   }
205   m_transfers.clear();
206
207   curl_multi_cleanup(m_multi_handle);
208   curl_global_cleanup();
209 }
210
211 void
212 Downloader::download(const std::string& url,
213                      size_t (*write_func)(void* ptr, size_t size, size_t nmemb, void* userdata),
214                      void* userdata)
215 {
216   log_info << "Downloading " << url << std::endl;
217
218   char error_buffer[CURL_ERROR_SIZE+1];
219
220   CURL* curl_handle = curl_easy_init();
221   curl_easy_setopt(curl_handle, CURLOPT_URL, url.c_str());
222   curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "SuperTux/" PACKAGE_VERSION " libcURL");
223   curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, write_func);
224   curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, userdata);
225   curl_easy_setopt(curl_handle, CURLOPT_ERRORBUFFER, error_buffer);
226   curl_easy_setopt(curl_handle, CURLOPT_NOPROGRESS, 1);
227   curl_easy_setopt(curl_handle, CURLOPT_NOSIGNAL, 1);
228   curl_easy_setopt(curl_handle, CURLOPT_FAILONERROR, 1);
229   curl_easy_setopt(curl_handle, CURLOPT_FOLLOWLOCATION, 1);
230   CURLcode result = curl_easy_perform(curl_handle);
231   curl_easy_cleanup(curl_handle);
232
233   if (result != CURLE_OK)
234   {
235     std::string why = error_buffer[0] ? error_buffer : "unhandled error";
236     throw std::runtime_error(url + ": download failed: " + why);
237   }
238 }
239
240 std::string
241 Downloader::download(const std::string& url)
242 {
243   std::string result;
244   download(url, my_curl_string_append, &result);
245   return result;
246 }
247
248 void
249 Downloader::download(const std::string& url, const std::string& filename)
250 {
251   log_info << "download: " << url << " to " << filename << std::endl;
252   std::unique_ptr<PHYSFS_file, int(*)(PHYSFS_File*)> fout(PHYSFS_openWrite(filename.c_str()),
253                                                           PHYSFS_close);
254   download(url, my_curl_physfs_write, fout.get());
255 }
256
257 void
258 Downloader::abort(TransferId id)
259 {
260   auto it = std::find_if(m_transfers.begin(), m_transfers.end(),
261                          [&id](const std::unique_ptr<Transfer>& rhs)
262                          {
263                            return id == rhs->get_id();
264                          });
265   if (it == m_transfers.end())
266   {
267     log_warning << "transfer not found: " << id << std::endl;
268   }
269   else
270   {
271     TransferStatusPtr status = (*it)->get_status();
272
273     curl_multi_remove_handle(m_multi_handle, (*it)->get_curl_handle());
274     m_transfers.erase(it);
275
276     for(auto& callback : status->callbacks)
277     {
278       try
279       {
280         callback(false);
281       }
282       catch(const std::exception& err)
283       {
284         log_warning << "Illegal exception in Downloader: " << err.what() << std::endl;
285       }
286     }
287   }
288 }
289
290 void
291 Downloader::update()
292 {
293   // read data from the network
294   CURLMcode ret;
295   int running_handles;
296   while((ret = curl_multi_perform(m_multi_handle, &running_handles)) == CURLM_CALL_MULTI_PERFORM)
297   {
298     log_debug << "updating" << std::endl;
299   }
300
301   // check if any downloads got finished
302   int msgs_in_queue;
303   CURLMsg* msg;
304   while ((msg = curl_multi_info_read(m_multi_handle, &msgs_in_queue)))
305   {
306     switch(msg->msg)
307     {
308       case CURLMSG_DONE:
309         {
310           log_info << "Download completed with " << msg->data.result << std::endl;
311           curl_multi_remove_handle(m_multi_handle, msg->easy_handle);
312
313           auto it = std::find_if(m_transfers.begin(), m_transfers.end(),
314                                  [&msg](const std::unique_ptr<Transfer>& rhs) {
315                                    return rhs->get_curl_handle() == msg->easy_handle;
316                                  });
317           assert(it != m_transfers.end());
318           TransferStatusPtr status = (*it)->get_status();
319           status->error_msg = (*it)->get_error_buffer();
320           m_transfers.erase(it);
321
322           if (msg->data.result == CURLE_OK)
323           {
324             bool success = true;
325             for(auto& callback : status->callbacks)
326             {
327               try
328               {
329                 callback(success);
330               }
331               catch(const std::exception& err)
332               {
333                 success = false;
334                 log_warning << "Exception in Downloader: " << err.what() << std::endl;
335                 status->error_msg = err.what();
336               }
337             }
338           }
339           else
340           {
341             log_warning << "Error: " << curl_easy_strerror(msg->data.result) << std::endl;
342             for(auto& callback : status->callbacks)
343             {
344               try
345               {
346                 callback(false);
347               }
348               catch(const std::exception& err)
349               {
350                 log_warning << "Illegal exception in Downloader: " << err.what() << std::endl;
351               }
352             }
353           }
354         }
355         break;
356
357       default:
358         log_warning << "unhandled cURL message: " << msg->msg << std::endl;
359         break;
360     }
361   }
362 }
363
364 TransferStatusPtr
365 Downloader::request_download(const std::string& url, const std::string& outfile)
366 {
367   log_info << "request_download: " << url << std::endl;
368   std::unique_ptr<Transfer> transfer(new Transfer(*this, m_next_transfer_id++, url, outfile));
369   curl_multi_add_handle(m_multi_handle, transfer->get_curl_handle());
370   m_transfers.push_back(std::move(transfer));
371   return m_transfers.back()->get_status();
372 }
373
374 /* EOF */