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