reapack.cpp (9921B)
1 /* ReaPack: Package manager for REAPER 2 * Copyright (C) 2015-2025 Christian Fillion 3 * 4 * This program is free software: you can redistribute it and/or modify 5 * it under the terms of the GNU Lesser General Public License as published by 6 * the Free Software Foundation, either version 3 of the License, or 7 * (at your option) any later version. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU Lesser General Public License for more details. 13 * 14 * You should have received a copy of the GNU Lesser General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18 #include "reapack.hpp" 19 20 #include "about.hpp" 21 #include "api.hpp" 22 #include "buildinfo.hpp" 23 #include "config.hpp" 24 #include "download.hpp" 25 #include "errors.hpp" 26 #include "filesystem.hpp" 27 #include "index.hpp" 28 #include "manager.hpp" 29 #include "obsquery.hpp" 30 #include "progress.hpp" 31 #include "report.hpp" 32 #include "richedit.hpp" 33 #include "transaction.hpp" 34 #include "win32.hpp" 35 36 #include <cassert> 37 38 #include <reaper_plugin_functions.h> 39 40 ReaPack *ReaPack::s_instance = nullptr; 41 42 #ifdef _WIN32 43 // Removes temporary files that could not be removed by an installation task 44 // (eg. extensions dll that were in use by REAPER). 45 // Surely there must be a better way... 46 static void CleanupTempFiles() 47 { 48 const Path &path = (Path::DATA + "*.tmp").prependRoot(); 49 const std::wstring &pattern = Win32::widen(path.join()); 50 51 WIN32_FIND_DATA fd = {}; 52 HANDLE handle = FindFirstFile(pattern.c_str(), &fd); 53 54 if(handle == INVALID_HANDLE_VALUE) 55 return; 56 57 do { 58 std::wstring file = pattern; 59 file.replace(file.size() - 5, 5, fd.cFileName); // 5 == strlen("*.tmp") 60 DeleteFile(file.c_str()); 61 } while(FindNextFile(handle, &fd)); 62 63 FindClose(handle); 64 } 65 #endif 66 67 Path ReaPack::resourcePath() 68 { 69 #ifdef _WIN32 70 // convert from the current system codepage to UTF-8 71 if(atof(GetAppVersion()) < 5.70) 72 return Win32::ansi2utf8(GetResourcePath()); 73 #endif 74 75 return {GetResourcePath()}; 76 } 77 78 ReaPack::ReaPack(REAPER_PLUGIN_HINSTANCE instance, HWND mainWindow) 79 : m_instance(instance), m_mainWindow(mainWindow), 80 m_useRootPath(resourcePath()), m_config(Path::CONFIG.prependRoot()), m_tx{} 81 { 82 assert(!s_instance); 83 s_instance = this; 84 85 DownloadContext::GlobalInit(); 86 RichEdit::Init(); 87 88 createDirectories(); 89 registerSelf(); 90 setupActions(); 91 setupAPI(); 92 93 if(m_config.isFirstRun()) 94 manageRemotes(); 95 96 #ifdef _WIN32 97 CleanupTempFiles(); 98 #endif 99 } 100 101 ReaPack::~ReaPack() 102 { 103 DownloadContext::GlobalCleanup(); 104 105 s_instance = nullptr; 106 } 107 108 void ReaPack::setupActions() 109 { 110 m_actions.add("REAPACK_SYNC", "ReaPack: Synchronize packages", 111 std::bind(&ReaPack::synchronizeAll, this)); 112 113 m_actions.add("REAPACK_BROWSE", "ReaPack: Browse packages...", 114 std::bind(&ReaPack::browsePackages, this)); 115 116 m_actions.add("REAPACK_UPLOAD", "ReaPack: Package editor", 117 std::bind(&ReaPack::uploadPackage, this)); 118 119 m_actions.add("REAPACK_IMPORT", "ReaPack: Import repositories...", 120 std::bind(&ReaPack::importRemote, this)); 121 122 m_actions.add("REAPACK_MANAGE", "ReaPack: Manage repositories...", 123 std::bind(&ReaPack::manageRemotes, this)); 124 125 m_actions.add("REAPACK_ABOUT", "ReaPack: About...", 126 std::bind(&ReaPack::aboutSelf, this)); 127 } 128 129 void ReaPack::setupAPI() 130 { 131 m_api.emplace_back(&API::AboutInstalledPackage); 132 m_api.emplace_back(&API::AboutRepository); 133 m_api.emplace_back(&API::AddSetRepository); 134 m_api.emplace_back(&API::BrowsePackages); 135 m_api.emplace_back(&API::CompareVersions); 136 m_api.emplace_back(&API::EnumOwnedFiles); 137 m_api.emplace_back(&API::FreeEntry); 138 m_api.emplace_back(&API::GetEntryInfo); 139 m_api.emplace_back(&API::GetOwner); 140 m_api.emplace_back(&API::GetRepositoryInfo); 141 m_api.emplace_back(&API::ProcessQueue); 142 } 143 144 void ReaPack::synchronizeAll() 145 { 146 const std::vector<Remote> &remotes = m_config.remotes.getEnabled(); 147 148 if(remotes.empty()) { 149 ShowMessageBox("No repository enabled, nothing to do!", "ReaPack", MB_OK); 150 return; 151 } 152 153 Transaction *tx = setupTransaction(); 154 155 if(!tx) 156 return; 157 158 for(const Remote &remote : remotes) 159 tx->synchronize(remote); 160 161 tx->runTasks(); 162 } 163 164 void ReaPack::addSetRemote(const Remote &remote) 165 { 166 if(remote.isEnabled() && remote.autoInstall(m_config.install.autoInstall)) { 167 const Remote &previous = m_config.remotes.get(remote.name()); 168 169 if(!previous || !previous.isEnabled() || previous.url() != remote.url()) { 170 if(Transaction *tx = setupTransaction()) 171 tx->synchronize(remote); 172 } 173 } 174 175 m_config.remotes.add(remote); 176 } 177 178 void ReaPack::uninstall(const Remote &remote) 179 { 180 if(remote.isProtected()) 181 return; 182 183 assert(m_tx); 184 m_tx->uninstall(remote); 185 186 m_tx->onFinish >> [=] { 187 if(!m_tx->isCancelled()) 188 config()->remotes.remove(remote); 189 }; 190 } 191 192 void ReaPack::uploadPackage() 193 { 194 Win32::shellExecute("https://reapack.com/upload"); 195 } 196 197 void ReaPack::importRemote() 198 { 199 const bool autoClose = m_manager == nullptr; 200 201 manageRemotes(); 202 203 if(!m_manager->importRepo() && autoClose) 204 m_manager->close(); 205 } 206 207 void ReaPack::manageRemotes() 208 { 209 if(m_manager) { 210 m_manager->setFocus(); 211 return; 212 } 213 214 m_manager = Dialog::Create<Manager>(m_instance, m_mainWindow, 215 [=](INT_PTR) { m_manager.reset(); }); 216 m_manager->show(); 217 } 218 219 Remote ReaPack::remote(const std::string &name) const 220 { 221 return m_config.remotes.get(name); 222 } 223 224 void ReaPack::about(const Remote &repo, const bool focus) 225 { 226 Transaction *tx = setupTransaction(); 227 if(!tx) 228 return; 229 230 const std::vector<Remote> repos{repo}; 231 232 tx->fetchIndexes(repos); 233 tx->onFinish >> [=] { 234 const auto &indexes = tx->getIndexes(repos); 235 if(!indexes.empty()) 236 about()->setDelegate(std::make_shared<AboutIndexDelegate>(indexes.front()), focus); 237 }; 238 tx->runTasks(); 239 } 240 241 void ReaPack::aboutSelf() 242 { 243 about(remote("ReaPack")); 244 } 245 246 About *ReaPack::about(const bool instantiate) 247 { 248 if(m_about) 249 return m_about.get(); 250 else if(!instantiate) 251 return nullptr; 252 253 m_about = Dialog::Create<About>(m_instance, m_mainWindow, 254 [=](INT_PTR) { m_about.reset(); }); 255 256 return m_about.get(); 257 } 258 259 Browser *ReaPack::browsePackages() 260 { 261 if(m_browser) 262 m_browser->setFocus(); 263 else { 264 m_browser = Dialog::Create<Browser>(m_instance, m_mainWindow, 265 [=](INT_PTR) { m_browser.reset(); }); 266 m_browser->refresh(); 267 } 268 269 return m_browser.get(); 270 } 271 272 Transaction *ReaPack::setupTransaction() 273 { 274 if(m_progress && m_progress->isVisible()) 275 m_progress->setFocus(); 276 277 if(m_tx) 278 return m_tx; 279 280 try { 281 m_tx = new Transaction; 282 } 283 catch(const reapack_error &e) { 284 Win32::messageBox(m_mainWindow, String::format( 285 "The following error occurred while creating a transaction:\n\n%s", 286 e.what() 287 ).c_str(), "ReaPack", MB_OK); 288 return nullptr; 289 } 290 291 assert(!m_progress); 292 m_progress = Dialog::Create<Progress>(m_instance, m_mainWindow, 293 nullptr, m_tx->threadPool()); 294 295 m_tx->onFinish >> [=] { 296 m_progress.reset(); 297 298 if(!m_tx->isCancelled() && !m_tx->receipt()->empty()) { 299 LockDialog managerLock(m_manager.get()); 300 LockDialog browserLock(m_browser.get()); 301 302 Dialog::Show<Report>(m_instance, m_mainWindow, m_tx->receipt()); 303 } 304 }; 305 306 m_tx->setObsoleteHandler([=] (std::vector<Registry::Entry> &entries) { 307 LockDialog aboutLock(m_about.get()); 308 LockDialog browserLock(m_browser.get()); 309 LockDialog managerLock(m_manager.get()); 310 LockDialog progressLock(m_progress.get()); 311 312 return Dialog::Show<ObsoleteQuery>(m_instance, m_mainWindow, 313 &entries, &config()->install.promptObsolete) == IDOK; 314 }); 315 316 m_tx->setCleanupHandler(std::bind(&ReaPack::teardownTransaction, this)); 317 318 return m_tx; 319 } 320 321 void ReaPack::teardownTransaction() 322 { 323 const bool needRefresh = m_tx->receipt()->test(Receipt::RefreshBrowser); 324 325 delete m_tx; 326 m_tx = nullptr; 327 328 // Update the browser only after the transaction is deleted because 329 // it must be able to start a new one to load the indexes 330 if(needRefresh) 331 refreshBrowser(); 332 } 333 334 void ReaPack::commitConfig(bool refresh) 335 { 336 if(m_tx) { 337 if(refresh) { 338 m_tx->receipt()->setIndexChanged(); // force browser refresh 339 m_tx->onFinish >> std::bind(&ReaPack::refreshManager, this); 340 } 341 m_tx->onFinish >> std::bind(&Config::write, &m_config); 342 m_tx->runTasks(); 343 } 344 else { 345 if(refresh) { 346 refreshManager(); 347 refreshBrowser(); 348 } 349 m_config.write(); 350 } 351 } 352 353 void ReaPack::refreshManager() 354 { 355 if(m_manager) 356 m_manager->refresh(); 357 } 358 359 void ReaPack::refreshBrowser() 360 { 361 if(m_browser) 362 m_browser->refresh(); 363 } 364 365 void ReaPack::createDirectories() 366 { 367 const Path &path = Path::CACHE; 368 369 if(FS::mkdir(path)) 370 return; 371 372 Win32::messageBox(Splash_GetWnd(), String::format( 373 "ReaPack could not create %s! " 374 "Please investigate or report this issue.\n\n" 375 "Error description: %s", 376 path.prependRoot().join().c_str(), FS::lastError() 377 ).c_str(), "ReaPack", MB_OK); 378 } 379 380 void ReaPack::registerSelf() 381 { 382 // hard-coding galore! 383 Index ri("ReaPack"); 384 Category cat("Extensions", &ri); 385 Package pkg(Package::ExtensionType, "ReaPack.ext", &cat); 386 Version ver(REAPACK_VERSION, &pkg); 387 ver.setAuthor("cfillion"); 388 ver.addSource(new Source(REAPACK_FILENAME, "dummy url", &ver)); 389 390 try { 391 Registry reg(Path::REGISTRY.prependRoot()); 392 const Registry::Entry &entry = reg.getEntry(&pkg); 393 if(entry && entry.version == ver.name()) 394 return; // avoid modifying the database file at every startup 395 reg.push(&ver); 396 reg.commit(); 397 } 398 catch(const reapack_error &) { 399 // Best to ignore the error for now. If something is wrong with the registry 400 // we'll show a message once when the user really wants to interact with ReaPack. 401 // 402 // Right now the user is likely to just want to use REAPER without being bothered. 403 } 404 }