reapack

Package manager for REAPER
Log | Files | Refs | Submodules | README | LICENSE

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 }