mirror of
				https://github.com/ciphervance/supercell-wx.git
				synced 2025-10-31 19:50:06 +00:00 
			
		
		
		
	Catch exceptions in background threads
This commit is contained in:
		
							parent
							
								
									863cdd0384
								
							
						
					
					
						commit
						e1d61fccfa
					
				
					 13 changed files with 866 additions and 665 deletions
				
			
		|  | @ -25,6 +25,8 @@ public: | |||
| 
 | ||||
|    ~Impl() { threadPool_.join(); } | ||||
| 
 | ||||
|    void DownloadSync(const std::shared_ptr<request::DownloadRequest>& request); | ||||
| 
 | ||||
|    boost::asio::thread_pool threadPool_ {1u}; | ||||
| 
 | ||||
|    DownloadManager* self_; | ||||
|  | @ -36,224 +38,229 @@ DownloadManager::~DownloadManager() = default; | |||
| void DownloadManager::Download( | ||||
|    const std::shared_ptr<request::DownloadRequest>& request) | ||||
| { | ||||
|    boost::asio::post( | ||||
|       p->threadPool_, | ||||
|       [=]() | ||||
|    boost::asio::post(p->threadPool_, | ||||
|                      [=]() | ||||
|                      { | ||||
|                         try | ||||
|                         { | ||||
|                            p->DownloadSync(request); | ||||
|                         } | ||||
|                         catch (const std::exception& ex) | ||||
|                         { | ||||
|                            logger_->error(ex.what()); | ||||
|                         } | ||||
|                      }); | ||||
| } | ||||
| 
 | ||||
| void DownloadManager::Impl::DownloadSync( | ||||
|    const std::shared_ptr<request::DownloadRequest>& request) | ||||
| { | ||||
|    // Prepare destination file
 | ||||
|    const std::filesystem::path& destinationPath = request->destination_path(); | ||||
| 
 | ||||
|    if (!destinationPath.has_parent_path()) | ||||
|    { | ||||
|       logger_->error("Destination has no parent path: \"{}\""); | ||||
| 
 | ||||
|       Q_EMIT request->RequestComplete( | ||||
|          request::DownloadRequest::CompleteReason::IOError); | ||||
| 
 | ||||
|       return; | ||||
|    } | ||||
| 
 | ||||
|    const std::filesystem::path parentPath = destinationPath.parent_path(); | ||||
| 
 | ||||
|    // Create directory if it doesn't exist
 | ||||
|    if (!std::filesystem::exists(parentPath)) | ||||
|    { | ||||
|       if (!std::filesystem::create_directories(parentPath)) | ||||
|       { | ||||
|          // Prepare destination file
 | ||||
|          const std::filesystem::path& destinationPath = | ||||
|             request->destination_path(); | ||||
|          logger_->error("Unable to create download directory: \"{}\"", | ||||
|                         parentPath.string()); | ||||
| 
 | ||||
|          if (!destinationPath.has_parent_path()) | ||||
|          { | ||||
|             logger_->error("Destination has no parent path: \"{}\""); | ||||
| 
 | ||||
|             Q_EMIT request->RequestComplete( | ||||
|                request::DownloadRequest::CompleteReason::IOError); | ||||
| 
 | ||||
|             return; | ||||
|          } | ||||
| 
 | ||||
|          const std::filesystem::path parentPath = destinationPath.parent_path(); | ||||
| 
 | ||||
|          // Create directory if it doesn't exist
 | ||||
|          if (!std::filesystem::exists(parentPath)) | ||||
|          { | ||||
|             if (!std::filesystem::create_directories(parentPath)) | ||||
|             { | ||||
|                logger_->error("Unable to create download directory: \"{}\"", | ||||
|                               parentPath.string()); | ||||
| 
 | ||||
|                Q_EMIT request->RequestComplete( | ||||
|                   request::DownloadRequest::CompleteReason::IOError); | ||||
| 
 | ||||
|                return; | ||||
|             } | ||||
|          } | ||||
| 
 | ||||
|          // Remove file if it exists
 | ||||
|          if (std::filesystem::exists(destinationPath)) | ||||
|          { | ||||
|             std::error_code error; | ||||
|             if (!std::filesystem::remove(destinationPath, error)) | ||||
|             { | ||||
|                logger_->error( | ||||
|                   "Unable to remove existing destination file ({}): \"{}\"", | ||||
|                   error.message(), | ||||
|                   destinationPath.string()); | ||||
| 
 | ||||
|                Q_EMIT request->RequestComplete( | ||||
|                   request::DownloadRequest::CompleteReason::IOError); | ||||
| 
 | ||||
|                return; | ||||
|             } | ||||
|          } | ||||
| 
 | ||||
|          // Open file for writing
 | ||||
|          std::ofstream ofs {destinationPath, | ||||
|                             std::ios_base::out | std::ios_base::binary | | ||||
|                                std::ios_base::trunc}; | ||||
|          if (!ofs.is_open() || !ofs.good()) | ||||
|          { | ||||
|             logger_->error( | ||||
|                "Unable to open destination file for writing: \"{}\"", | ||||
|                destinationPath.string()); | ||||
| 
 | ||||
|             Q_EMIT request->RequestComplete( | ||||
|                request::DownloadRequest::CompleteReason::IOError); | ||||
| 
 | ||||
|             return; | ||||
|          } | ||||
| 
 | ||||
|          std::chrono::system_clock::time_point lastUpdated {}; | ||||
|          cpr::cpr_off_t                        lastDownloadNow {}; | ||||
|          cpr::cpr_off_t                        lastDownloadTotal {}; | ||||
| 
 | ||||
|          // Download file
 | ||||
|          cpr::Response response = | ||||
|             cpr::Get(cpr::Url {request->url()}, | ||||
|                      cpr::ProgressCallback( | ||||
|                         [&](cpr::cpr_off_t downloadTotal, | ||||
|                             cpr::cpr_off_t downloadNow, | ||||
|                             cpr::cpr_off_t /* uploadTotal */, | ||||
|                             cpr::cpr_off_t /* uploadNow */, | ||||
|                             std::intptr_t /* userdata */) | ||||
|                         { | ||||
|                            using namespace std::chrono_literals; | ||||
| 
 | ||||
|                            std::chrono::system_clock::time_point now = | ||||
|                               std::chrono::system_clock::now(); | ||||
| 
 | ||||
|                            // Only emit an update every 100ms
 | ||||
|                            if ((now > lastUpdated + 100ms || | ||||
|                                 downloadNow == downloadTotal) && | ||||
|                                (downloadNow != lastDownloadNow || | ||||
|                                 downloadTotal != lastDownloadTotal)) | ||||
|                            { | ||||
|                               logger_->trace("Downloaded: {} / {}", | ||||
|                                              downloadNow, | ||||
|                                              downloadTotal); | ||||
| 
 | ||||
|                               Q_EMIT request->ProgressUpdated(downloadNow, | ||||
|                                                               downloadTotal); | ||||
| 
 | ||||
|                               lastUpdated       = now; | ||||
|                               lastDownloadNow   = downloadNow; | ||||
|                               lastDownloadTotal = downloadTotal; | ||||
|                            } | ||||
| 
 | ||||
|                            return !request->IsCanceled(); | ||||
|                         }), | ||||
|                      cpr::WriteCallback( | ||||
|                         [&](std::string data, std::intptr_t /* userdata */) | ||||
|                         { | ||||
|                            // Write file
 | ||||
|                            ofs << data; | ||||
|                            return !request->IsCanceled(); | ||||
|                         })); | ||||
| 
 | ||||
|          bool ofsGood = ofs.good(); | ||||
|          ofs.close(); | ||||
| 
 | ||||
|          // Handle error response
 | ||||
|          if (response.error.code != cpr::ErrorCode::OK || | ||||
|              request->IsCanceled() || !ofsGood) | ||||
|          { | ||||
|             request::DownloadRequest::CompleteReason reason = | ||||
|                request::DownloadRequest::CompleteReason::IOError; | ||||
| 
 | ||||
|             if (request->IsCanceled()) | ||||
|             { | ||||
|                logger_->info("Download request cancelled: {}", request->url()); | ||||
| 
 | ||||
|                reason = request::DownloadRequest::CompleteReason::Canceled; | ||||
|             } | ||||
|             else if (response.error.code != cpr::ErrorCode::OK) | ||||
|             { | ||||
|                logger_->error("Error downloading file ({}): {}", | ||||
|                               response.error.message, | ||||
|                               request->url()); | ||||
| 
 | ||||
|                reason = request::DownloadRequest::CompleteReason::RemoteError; | ||||
|             } | ||||
|             else if (!ofsGood) | ||||
|             { | ||||
|                logger_->error("File I/O error: {}", destinationPath.string()); | ||||
| 
 | ||||
|                reason = request::DownloadRequest::CompleteReason::IOError; | ||||
|             } | ||||
| 
 | ||||
|             std::error_code error; | ||||
|             if (!std::filesystem::remove(destinationPath, error)) | ||||
|             { | ||||
|                logger_->error("Unable to remove destination file: {}, {}", | ||||
|                               destinationPath.string(), | ||||
|                               error.message()); | ||||
|             } | ||||
| 
 | ||||
|             Q_EMIT request->RequestComplete(reason); | ||||
| 
 | ||||
|             return; | ||||
|          } | ||||
| 
 | ||||
|          // Handle response
 | ||||
|          const auto contentMd5 = response.header.find("content-md5"); | ||||
|          if (contentMd5 != response.header.cend() && | ||||
|              !contentMd5->second.empty()) | ||||
|          { | ||||
|             // Open file for reading
 | ||||
|             std::ifstream is {destinationPath, | ||||
|                               std::ios_base::in | std::ios_base::binary}; | ||||
|             if (!is.is_open() || !is.good()) | ||||
|             { | ||||
|                logger_->error("Unable to open destination file for reading: {}", | ||||
|                               destinationPath.string()); | ||||
| 
 | ||||
|                Q_EMIT request->RequestComplete( | ||||
|                   request::DownloadRequest::CompleteReason::IOError); | ||||
| 
 | ||||
|                return; | ||||
|             } | ||||
| 
 | ||||
|             // Compute MD5
 | ||||
|             std::vector<std::uint8_t> digest {}; | ||||
|             if (!util::ComputeDigest(EVP_md5(), is, digest)) | ||||
|             { | ||||
|                logger_->error("Failed to compute MD5: {}", | ||||
|                               destinationPath.string()); | ||||
| 
 | ||||
|                Q_EMIT request->RequestComplete( | ||||
|                   request::DownloadRequest::CompleteReason::IOError); | ||||
| 
 | ||||
|                return; | ||||
|             } | ||||
| 
 | ||||
|             // Compare calculated MD5 with digest in response header
 | ||||
|             QByteArray expectedDigestArray = | ||||
|                QByteArray::fromBase64(contentMd5->second.c_str()); | ||||
|             std::vector<std::uint8_t> expectedDigest( | ||||
|                expectedDigestArray.cbegin(), expectedDigestArray.cend()); | ||||
| 
 | ||||
|             if (digest != expectedDigest) | ||||
|             { | ||||
|                QByteArray calculatedDigest( | ||||
|                   reinterpret_cast<char*>(digest.data()), digest.size()); | ||||
| 
 | ||||
|                logger_->error("Digest mismatch: {} != {}", | ||||
|                               calculatedDigest.toBase64().toStdString(), | ||||
|                               contentMd5->second); | ||||
| 
 | ||||
|                Q_EMIT request->RequestComplete( | ||||
|                   request::DownloadRequest::CompleteReason::DigestError); | ||||
| 
 | ||||
|                return; | ||||
|             } | ||||
|          } | ||||
| 
 | ||||
|          logger_->info("Download complete: {}", request->url()); | ||||
|          Q_EMIT request->RequestComplete( | ||||
|             request::DownloadRequest::CompleteReason::OK); | ||||
|       }); | ||||
|             request::DownloadRequest::CompleteReason::IOError); | ||||
| 
 | ||||
|          return; | ||||
|       } | ||||
|    } | ||||
| 
 | ||||
|    // Remove file if it exists
 | ||||
|    if (std::filesystem::exists(destinationPath)) | ||||
|    { | ||||
|       std::error_code error; | ||||
|       if (!std::filesystem::remove(destinationPath, error)) | ||||
|       { | ||||
|          logger_->error( | ||||
|             "Unable to remove existing destination file ({}): \"{}\"", | ||||
|             error.message(), | ||||
|             destinationPath.string()); | ||||
| 
 | ||||
|          Q_EMIT request->RequestComplete( | ||||
|             request::DownloadRequest::CompleteReason::IOError); | ||||
| 
 | ||||
|          return; | ||||
|       } | ||||
|    } | ||||
| 
 | ||||
|    // Open file for writing
 | ||||
|    std::ofstream ofs {destinationPath, | ||||
|                       std::ios_base::out | std::ios_base::binary | | ||||
|                          std::ios_base::trunc}; | ||||
|    if (!ofs.is_open() || !ofs.good()) | ||||
|    { | ||||
|       logger_->error("Unable to open destination file for writing: \"{}\"", | ||||
|                      destinationPath.string()); | ||||
| 
 | ||||
|       Q_EMIT request->RequestComplete( | ||||
|          request::DownloadRequest::CompleteReason::IOError); | ||||
| 
 | ||||
|       return; | ||||
|    } | ||||
| 
 | ||||
|    std::chrono::system_clock::time_point lastUpdated {}; | ||||
|    cpr::cpr_off_t                        lastDownloadNow {}; | ||||
|    cpr::cpr_off_t                        lastDownloadTotal {}; | ||||
| 
 | ||||
|    // Download file
 | ||||
|    cpr::Response response = cpr::Get( | ||||
|       cpr::Url {request->url()}, | ||||
|       cpr::ProgressCallback( | ||||
|          [&](cpr::cpr_off_t downloadTotal, | ||||
|              cpr::cpr_off_t downloadNow, | ||||
|              cpr::cpr_off_t /* uploadTotal */, | ||||
|              cpr::cpr_off_t /* uploadNow */, | ||||
|              std::intptr_t /* userdata */) | ||||
|          { | ||||
|             using namespace std::chrono_literals; | ||||
| 
 | ||||
|             std::chrono::system_clock::time_point now = | ||||
|                std::chrono::system_clock::now(); | ||||
| 
 | ||||
|             // Only emit an update every 100ms
 | ||||
|             if ((now > lastUpdated + 100ms || downloadNow == downloadTotal) && | ||||
|                 (downloadNow != lastDownloadNow || | ||||
|                  downloadTotal != lastDownloadTotal)) | ||||
|             { | ||||
|                logger_->trace( | ||||
|                   "Downloaded: {} / {}", downloadNow, downloadTotal); | ||||
| 
 | ||||
|                Q_EMIT request->ProgressUpdated(downloadNow, downloadTotal); | ||||
| 
 | ||||
|                lastUpdated       = now; | ||||
|                lastDownloadNow   = downloadNow; | ||||
|                lastDownloadTotal = downloadTotal; | ||||
|             } | ||||
| 
 | ||||
|             return !request->IsCanceled(); | ||||
|          }), | ||||
|       cpr::WriteCallback( | ||||
|          [&](std::string data, std::intptr_t /* userdata */) | ||||
|          { | ||||
|             // Write file
 | ||||
|             ofs << data; | ||||
|             return !request->IsCanceled(); | ||||
|          })); | ||||
| 
 | ||||
|    bool ofsGood = ofs.good(); | ||||
|    ofs.close(); | ||||
| 
 | ||||
|    // Handle error response
 | ||||
|    if (response.error.code != cpr::ErrorCode::OK || request->IsCanceled() || | ||||
|        !ofsGood) | ||||
|    { | ||||
|       request::DownloadRequest::CompleteReason reason = | ||||
|          request::DownloadRequest::CompleteReason::IOError; | ||||
| 
 | ||||
|       if (request->IsCanceled()) | ||||
|       { | ||||
|          logger_->info("Download request cancelled: {}", request->url()); | ||||
| 
 | ||||
|          reason = request::DownloadRequest::CompleteReason::Canceled; | ||||
|       } | ||||
|       else if (response.error.code != cpr::ErrorCode::OK) | ||||
|       { | ||||
|          logger_->error("Error downloading file ({}): {}", | ||||
|                         response.error.message, | ||||
|                         request->url()); | ||||
| 
 | ||||
|          reason = request::DownloadRequest::CompleteReason::RemoteError; | ||||
|       } | ||||
|       else if (!ofsGood) | ||||
|       { | ||||
|          logger_->error("File I/O error: {}", destinationPath.string()); | ||||
| 
 | ||||
|          reason = request::DownloadRequest::CompleteReason::IOError; | ||||
|       } | ||||
| 
 | ||||
|       std::error_code error; | ||||
|       if (!std::filesystem::remove(destinationPath, error)) | ||||
|       { | ||||
|          logger_->error("Unable to remove destination file: {}, {}", | ||||
|                         destinationPath.string(), | ||||
|                         error.message()); | ||||
|       } | ||||
| 
 | ||||
|       Q_EMIT request->RequestComplete(reason); | ||||
| 
 | ||||
|       return; | ||||
|    } | ||||
| 
 | ||||
|    // Handle response
 | ||||
|    const auto contentMd5 = response.header.find("content-md5"); | ||||
|    if (contentMd5 != response.header.cend() && !contentMd5->second.empty()) | ||||
|    { | ||||
|       // Open file for reading
 | ||||
|       std::ifstream is {destinationPath, | ||||
|                         std::ios_base::in | std::ios_base::binary}; | ||||
|       if (!is.is_open() || !is.good()) | ||||
|       { | ||||
|          logger_->error("Unable to open destination file for reading: {}", | ||||
|                         destinationPath.string()); | ||||
| 
 | ||||
|          Q_EMIT request->RequestComplete( | ||||
|             request::DownloadRequest::CompleteReason::IOError); | ||||
| 
 | ||||
|          return; | ||||
|       } | ||||
| 
 | ||||
|       // Compute MD5
 | ||||
|       std::vector<std::uint8_t> digest {}; | ||||
|       if (!util::ComputeDigest(EVP_md5(), is, digest)) | ||||
|       { | ||||
|          logger_->error("Failed to compute MD5: {}", destinationPath.string()); | ||||
| 
 | ||||
|          Q_EMIT request->RequestComplete( | ||||
|             request::DownloadRequest::CompleteReason::IOError); | ||||
| 
 | ||||
|          return; | ||||
|       } | ||||
| 
 | ||||
|       // Compare calculated MD5 with digest in response header
 | ||||
|       QByteArray expectedDigestArray = | ||||
|          QByteArray::fromBase64(contentMd5->second.c_str()); | ||||
|       std::vector<std::uint8_t> expectedDigest(expectedDigestArray.cbegin(), | ||||
|                                                expectedDigestArray.cend()); | ||||
| 
 | ||||
|       if (digest != expectedDigest) | ||||
|       { | ||||
|          QByteArray calculatedDigest(reinterpret_cast<char*>(digest.data()), | ||||
|                                      digest.size()); | ||||
| 
 | ||||
|          logger_->error("Digest mismatch: {} != {}", | ||||
|                         calculatedDigest.toBase64().toStdString(), | ||||
|                         contentMd5->second); | ||||
| 
 | ||||
|          Q_EMIT request->RequestComplete( | ||||
|             request::DownloadRequest::CompleteReason::DigestError); | ||||
| 
 | ||||
|          return; | ||||
|       } | ||||
|    } | ||||
| 
 | ||||
|    logger_->info("Download complete: {}", request->url()); | ||||
|    Q_EMIT request->RequestComplete( | ||||
|       request::DownloadRequest::CompleteReason::OK); | ||||
| } | ||||
| 
 | ||||
| std::shared_ptr<DownloadManager> DownloadManager::Instance() | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Dan Paulat
						Dan Paulat