| @@ -0,0 +1,51 @@ | |||
| #pragma once | |||
| #include <common.hpp> | |||
| #include <vector> | |||
| namespace rack { | |||
| /** Synchronizes plugins with the VCV Library and updates Rack itself | |||
| */ | |||
| namespace library { | |||
| struct Update { | |||
| std::string pluginSlug; | |||
| std::string pluginName; | |||
| std::string version; | |||
| std::string changelogUrl; | |||
| float progress = 0.f; | |||
| }; | |||
| extern std::string version; | |||
| extern std::string changelogUrl; | |||
| extern float progress; | |||
| void init(); | |||
| bool isLoggedIn(); | |||
| void logIn(const std::string& email, const std::string& password); | |||
| void logOut(); | |||
| bool isUpdateAvailable(); | |||
| void queryUpdates(); | |||
| bool hasUpdates(); | |||
| void syncUpdate(Update* update); | |||
| void syncUpdates(); | |||
| bool isSyncing(); | |||
| /** Updates Rack automatically or opens the browser to the URL. | |||
| Blocking. Call on a detached thread. | |||
| */ | |||
| void update(); | |||
| extern std::string loginStatus; | |||
| extern std::vector<Update> updates; | |||
| extern std::string updateStatus; | |||
| extern bool restartRequested; | |||
| } // namespace library | |||
| } // namespace rack | |||
| @@ -9,30 +9,13 @@ | |||
| namespace rack { | |||
| /** Plugin loader and VCV Library synchronizer | |||
| /** Loads and manages plugins | |||
| */ | |||
| namespace plugin { | |||
| struct Update { | |||
| std::string pluginSlug; | |||
| std::string pluginName; | |||
| std::string version; | |||
| std::string changelogUrl; | |||
| float progress = 0.f; | |||
| }; | |||
| void init(); | |||
| void destroy(); | |||
| void logIn(const std::string& email, const std::string& password); | |||
| void logOut(); | |||
| bool isLoggedIn(); | |||
| void queryUpdates(); | |||
| bool hasUpdates(); | |||
| void syncUpdate(Update* update); | |||
| void syncUpdates(); | |||
| bool isSyncing(); | |||
| Plugin* getPlugin(const std::string& pluginSlug); | |||
| Model* getModel(const std::string& pluginSlug, const std::string& modelSlug); | |||
| /** Creates a Model from a JSON module object. | |||
| @@ -47,11 +30,6 @@ std::string normalizeSlug(const std::string& slug); | |||
| extern std::vector<Plugin*> plugins; | |||
| extern std::string loginStatus; | |||
| extern std::vector<Update> updates; | |||
| extern std::string updateStatus; | |||
| extern bool restartRequested; | |||
| } // namespace plugin | |||
| } // namespace rack | |||
| @@ -1,26 +0,0 @@ | |||
| #pragma once | |||
| #include <common.hpp> | |||
| namespace rack { | |||
| /** Automatically updates the application. | |||
| */ | |||
| namespace updater { | |||
| extern std::string version; | |||
| extern std::string changelogUrl; | |||
| extern float progress; | |||
| void init(); | |||
| bool isUpdateAvailable(); | |||
| /** Updates Rack automatically or opens the browser to the URL. | |||
| Blocking. Call on a detached thread. | |||
| */ | |||
| void update(); | |||
| } // namespace updater | |||
| } // namespace rack | |||
| @@ -21,7 +21,7 @@ | |||
| #include <system.hpp> | |||
| #include <plugin.hpp> | |||
| #include <patch.hpp> | |||
| #include <updater.hpp> | |||
| #include <library.hpp> | |||
| namespace rack { | |||
| @@ -608,7 +608,7 @@ struct LogInItem : ui::MenuItem { | |||
| std::string email = emailField->text; | |||
| std::string password = passwordField->text; | |||
| std::thread t([ = ] { | |||
| plugin::logIn(email, password); | |||
| library::logIn(email, password); | |||
| isLoggingIn = false; | |||
| }); | |||
| t.detach(); | |||
| @@ -618,7 +618,7 @@ struct LogInItem : ui::MenuItem { | |||
| void step() override { | |||
| disabled = isLoggingIn; | |||
| text = "Log in"; | |||
| rightText = plugin::loginStatus; | |||
| rightText = library::loginStatus; | |||
| MenuItem::step(); | |||
| } | |||
| }; | |||
| @@ -626,13 +626,13 @@ struct LogInItem : ui::MenuItem { | |||
| struct SyncItem : ui::MenuItem { | |||
| void step() override { | |||
| disabled = true; | |||
| if (plugin::updateStatus != "") { | |||
| text = plugin::updateStatus; | |||
| if (library::updateStatus != "") { | |||
| text = library::updateStatus; | |||
| } | |||
| else if (plugin::isSyncing()) { | |||
| else if (library::isSyncing()) { | |||
| text = "Updating..."; | |||
| } | |||
| else if (!plugin::hasUpdates()) { | |||
| else if (!library::hasUpdates()) { | |||
| text = "Up-to-date"; | |||
| } | |||
| else { | |||
| @@ -644,7 +644,7 @@ struct SyncItem : ui::MenuItem { | |||
| void onAction(const event::Action& e) override { | |||
| std::thread t([ = ] { | |||
| plugin::syncUpdates(); | |||
| library::syncUpdates(); | |||
| }); | |||
| t.detach(); | |||
| e.consume(NULL); | |||
| @@ -652,9 +652,9 @@ struct SyncItem : ui::MenuItem { | |||
| }; | |||
| struct PluginSyncItem : ui::MenuItem { | |||
| plugin::Update* update; | |||
| library::Update* update; | |||
| void setUpdate(plugin::Update* update) { | |||
| void setUpdate(library::Update* update) { | |||
| this->update = update; | |||
| text = update->pluginName; | |||
| plugin::Plugin* p = plugin::getPlugin(update->pluginSlug); | |||
| @@ -679,7 +679,7 @@ struct PluginSyncItem : ui::MenuItem { | |||
| } | |||
| void step() override { | |||
| disabled = plugin::isSyncing(); | |||
| disabled = library::isSyncing(); | |||
| if (update->progress >= 1) { | |||
| rightText = CHECKMARK_STRING; | |||
| disabled = true; | |||
| @@ -692,7 +692,7 @@ struct PluginSyncItem : ui::MenuItem { | |||
| void onAction(const event::Action& e) override { | |||
| std::thread t([ = ] { | |||
| plugin::syncUpdate(update); | |||
| library::syncUpdate(update); | |||
| }); | |||
| t.detach(); | |||
| e.consume(NULL); | |||
| @@ -701,7 +701,7 @@ struct PluginSyncItem : ui::MenuItem { | |||
| struct LogOutItem : ui::MenuItem { | |||
| void onAction(const event::Action& e) override { | |||
| plugin::logOut(); | |||
| library::logOut(); | |||
| } | |||
| }; | |||
| @@ -714,7 +714,7 @@ struct LibraryMenu : ui::Menu { | |||
| void step() override { | |||
| // Refresh menu when appropriate | |||
| if (!loggedIn && plugin::isLoggedIn()) | |||
| if (!loggedIn && library::isLoggedIn()) | |||
| refresh(); | |||
| Menu::step(); | |||
| } | |||
| @@ -726,7 +726,7 @@ struct LibraryMenu : ui::Menu { | |||
| if (settings::devMode) { | |||
| addChild(createMenuLabel("Disabled in development mode")); | |||
| } | |||
| else if (!plugin::isLoggedIn()) { | |||
| else if (!library::isLoggedIn()) { | |||
| UrlItem* registerItem = new UrlItem; | |||
| registerItem->text = "Register VCV account"; | |||
| registerItem->url = "https://vcvrack.com/"; | |||
| @@ -765,14 +765,14 @@ struct LibraryMenu : ui::Menu { | |||
| syncItem->text = "Update all"; | |||
| addChild(syncItem); | |||
| if (plugin::hasUpdates()) { | |||
| if (library::hasUpdates()) { | |||
| addChild(new ui::MenuSeparator); | |||
| ui::MenuLabel* updatesLabel = new ui::MenuLabel; | |||
| updatesLabel->text = "Updates"; | |||
| addChild(updatesLabel); | |||
| for (plugin::Update& update : plugin::updates) { | |||
| for (library::Update& update : library::updates) { | |||
| PluginSyncItem* updateItem = new PluginSyncItem; | |||
| updateItem->setUpdate(&update); | |||
| addChild(updateItem); | |||
| @@ -799,11 +799,11 @@ struct LibraryButton : MenuButton { | |||
| void step() override { | |||
| notification->box.pos = math::Vec(0, 0); | |||
| notification->visible = plugin::hasUpdates(); | |||
| notification->visible = library::hasUpdates(); | |||
| // Popup when updates finish downloading | |||
| if (plugin::restartRequested) { | |||
| plugin::restartRequested = false; | |||
| if (library::restartRequested) { | |||
| library::restartRequested = false; | |||
| if (osdialog_message(OSDIALOG_INFO, OSDIALOG_OK_CANCEL, "All plugins have been downloaded. Close and re-launch Rack to load new updates.")) { | |||
| APP->window->close(); | |||
| } | |||
| @@ -823,22 +823,22 @@ struct UpdateItem : ui::MenuItem { | |||
| UrlItem* changelogUrl = new UrlItem; | |||
| changelogUrl->text = "Changelog"; | |||
| changelogUrl->url = updater::changelogUrl; | |||
| changelogUrl->url = library::changelogUrl; | |||
| menu->addChild(changelogUrl); | |||
| return menu; | |||
| } | |||
| void step() override { | |||
| if (updater::progress > 0) { | |||
| rightText = string::f("%.0f%%", updater::progress * 100.f); | |||
| if (library::progress > 0) { | |||
| rightText = string::f("%.0f%%", library::progress * 100.f); | |||
| } | |||
| MenuItem::step(); | |||
| } | |||
| void onAction(const event::Action& e) override { | |||
| std::thread t([ = ] { | |||
| updater::update(); | |||
| library::update(); | |||
| }); | |||
| t.detach(); | |||
| e.consume(NULL); | |||
| @@ -859,10 +859,10 @@ struct HelpButton : MenuButton { | |||
| menu->box.pos = getAbsoluteOffset(math::Vec(0, box.size.y)); | |||
| menu->box.size.x = box.size.x; | |||
| if (updater::isUpdateAvailable()) { | |||
| if (library::isUpdateAvailable()) { | |||
| UpdateItem* updateItem = new UpdateItem; | |||
| updateItem->text = "Update " + APP_NAME; | |||
| updateItem->rightText = APP_VERSION + " → " + updater::version; | |||
| updateItem->rightText = APP_VERSION + " → " + library::version; | |||
| menu->addChild(updateItem); | |||
| } | |||
| @@ -885,7 +885,7 @@ struct HelpButton : MenuButton { | |||
| void step() override { | |||
| notification->box.pos = math::Vec(0, 0); | |||
| notification->visible = updater::isUpdateAvailable(); | |||
| notification->visible = library::isUpdateAvailable(); | |||
| MenuButton::step(); | |||
| } | |||
| }; | |||
| @@ -0,0 +1,359 @@ | |||
| #include <thread> | |||
| #include <library.hpp> | |||
| #include <settings.hpp> | |||
| #include <app/common.hpp> | |||
| #include <network.hpp> | |||
| #include <system.hpp> | |||
| #include <context.hpp> | |||
| #include <window.hpp> | |||
| #include <asset.hpp> | |||
| #include <settings.hpp> | |||
| #include <plugin.hpp> | |||
| namespace rack { | |||
| namespace library { | |||
| std::string version; | |||
| std::string changelogUrl; | |||
| float progress = 0.f; | |||
| static std::string downloadUrl; | |||
| static void checkVersion() { | |||
| std::string versionUrl = API_URL + "/version"; | |||
| json_t* resJ = network::requestJson(network::METHOD_GET, versionUrl, NULL); | |||
| if (!resJ) { | |||
| WARN("Request for version failed"); | |||
| return; | |||
| } | |||
| DEFER({json_decref(resJ);}); | |||
| json_t* versionJ = json_object_get(resJ, "version"); | |||
| if (versionJ) | |||
| version = json_string_value(versionJ); | |||
| json_t* changelogUrlJ = json_object_get(resJ, "changelogUrl"); | |||
| if (changelogUrlJ) | |||
| changelogUrl = json_string_value(changelogUrlJ); | |||
| json_t* downloadUrlsJ = json_object_get(resJ, "downloadUrls"); | |||
| if (downloadUrlsJ) { | |||
| json_t* downloadUrlJ = json_object_get(downloadUrlsJ, APP_ARCH.c_str()); | |||
| if (downloadUrlJ) | |||
| downloadUrl = json_string_value(downloadUrlJ); | |||
| } | |||
| } | |||
| void init() { | |||
| if (settings::devMode) | |||
| return; | |||
| std::thread t([]() { | |||
| checkVersion(); | |||
| }); | |||
| t.detach(); | |||
| // Sync in a detached thread | |||
| if (!settings::devMode) { | |||
| std::thread t([] { | |||
| queryUpdates(); | |||
| }); | |||
| t.detach(); | |||
| } | |||
| } | |||
| bool isLoggedIn() { | |||
| return settings::token != ""; | |||
| } | |||
| void logIn(const std::string& email, const std::string& password) { | |||
| loginStatus = "Logging in..."; | |||
| json_t* reqJ = json_object(); | |||
| json_object_set(reqJ, "email", json_string(email.c_str())); | |||
| json_object_set(reqJ, "password", json_string(password.c_str())); | |||
| std::string url = API_URL + "/token"; | |||
| json_t* resJ = network::requestJson(network::METHOD_POST, url, reqJ); | |||
| json_decref(reqJ); | |||
| if (!resJ) { | |||
| loginStatus = "No response from server"; | |||
| return; | |||
| } | |||
| DEFER({ | |||
| json_decref(resJ); | |||
| }); | |||
| json_t* errorJ = json_object_get(resJ, "error"); | |||
| if (errorJ) { | |||
| const char* errorStr = json_string_value(errorJ); | |||
| loginStatus = errorStr; | |||
| return; | |||
| } | |||
| json_t* tokenJ = json_object_get(resJ, "token"); | |||
| if (!tokenJ) { | |||
| loginStatus = "No token in response"; | |||
| return; | |||
| } | |||
| const char* tokenStr = json_string_value(tokenJ); | |||
| settings::token = tokenStr; | |||
| loginStatus = ""; | |||
| queryUpdates(); | |||
| } | |||
| void logOut() { | |||
| settings::token = ""; | |||
| updates.clear(); | |||
| } | |||
| void update() { | |||
| if (downloadUrl == "") | |||
| return; | |||
| // Download update | |||
| // HACK getFilename is only supposed to be used for filesystem paths, not URLs. | |||
| std::string filename = system::getFilename(network::urlPath(downloadUrl)); | |||
| std::string path = asset::user(filename); | |||
| INFO("Downloading update %s to %s", downloadUrl.c_str(), path.c_str()); | |||
| network::requestDownload(downloadUrl, path, &progress); | |||
| #if defined ARCH_WIN | |||
| // Launch the installer | |||
| INFO("Launching update %s", path.c_str()); | |||
| system::runProcessDetached(path); | |||
| #elif defined ARCH_MAC | |||
| std::string cmd; | |||
| // std::string appPath = system::join(asset::userDir, "Rack.app"); | |||
| // cmd = "rm -rf '" + appPath + "'"; | |||
| // std::system(cmd.c_str()); | |||
| // // Unzip app using Apple's unzipper, since Rack's unzipper doesn't handle the metadata stuff correctly. | |||
| // cmd = "unzip -q '" + path + "' -d '" + asset::userDir + "'"; | |||
| // std::system(cmd.c_str()); | |||
| // // Open app in Finder | |||
| // cmd = "open -R '" + appPath + "'"; | |||
| // std::system(cmd.c_str()); | |||
| // Open Archive Utility | |||
| cmd = "open '" + path + "'"; | |||
| std::system(cmd.c_str()); | |||
| #elif defined ARCH_LIN | |||
| system::openFolder(asset::user("")); | |||
| #endif | |||
| APP->window->close(); | |||
| } | |||
| bool isUpdateAvailable() { | |||
| return (version != "") && (version != APP_VERSION); | |||
| } | |||
| void queryUpdates() { | |||
| if (settings::token.empty()) | |||
| return; | |||
| updates.clear(); | |||
| updateStatus = "Querying for updates..."; | |||
| // Get user's plugins list | |||
| std::string pluginsUrl = API_URL + "/plugins"; | |||
| network::CookieMap cookies; | |||
| cookies["token"] = settings::token; | |||
| json_t* pluginsResJ = network::requestJson(network::METHOD_GET, pluginsUrl, NULL, cookies); | |||
| if (!pluginsResJ) { | |||
| WARN("Request for user's plugins failed"); | |||
| updateStatus = "Could not query updates"; | |||
| return; | |||
| } | |||
| DEFER({ | |||
| json_decref(pluginsResJ); | |||
| }); | |||
| json_t* errorJ = json_object_get(pluginsResJ, "error"); | |||
| if (errorJ) { | |||
| WARN("Request for user's plugins returned an error: %s", json_string_value(errorJ)); | |||
| updateStatus = "Could not query updates"; | |||
| return; | |||
| } | |||
| // Get library manifests | |||
| std::string manifestsUrl = API_URL + "/library/manifests"; | |||
| json_t* manifestsReq = json_object(); | |||
| json_object_set(manifestsReq, "version", json_string(API_VERSION.c_str())); | |||
| json_t* manifestsResJ = network::requestJson(network::METHOD_GET, manifestsUrl, manifestsReq); | |||
| json_decref(manifestsReq); | |||
| if (!manifestsResJ) { | |||
| WARN("Request for library manifests failed"); | |||
| updateStatus = "Could not query updates"; | |||
| return; | |||
| } | |||
| DEFER({ | |||
| json_decref(manifestsResJ); | |||
| }); | |||
| json_t* manifestsJ = json_object_get(manifestsResJ, "manifests"); | |||
| json_t* pluginsJ = json_object_get(pluginsResJ, "plugins"); | |||
| size_t pluginIndex; | |||
| json_t* pluginJ; | |||
| json_array_foreach(pluginsJ, pluginIndex, pluginJ) { | |||
| Update update; | |||
| // Get plugin manifest | |||
| update.pluginSlug = json_string_value(pluginJ); | |||
| json_t* manifestJ = json_object_get(manifestsJ, update.pluginSlug.c_str()); | |||
| if (!manifestJ) { | |||
| WARN("VCV account has plugin %s but no manifest was found", update.pluginSlug.c_str()); | |||
| continue; | |||
| } | |||
| // Get plugin name | |||
| json_t* nameJ = json_object_get(manifestJ, "name"); | |||
| if (nameJ) | |||
| update.pluginName = json_string_value(nameJ); | |||
| // Get version | |||
| json_t* versionJ = json_object_get(manifestJ, "version"); | |||
| if (!versionJ) { | |||
| WARN("Plugin %s has no version in manifest", update.pluginSlug.c_str()); | |||
| continue; | |||
| } | |||
| update.version = json_string_value(versionJ); | |||
| // Check if update is needed | |||
| plugin::Plugin* p = plugin::getPlugin(update.pluginSlug); | |||
| if (p && p->version == update.version) | |||
| continue; | |||
| // Check status | |||
| json_t* statusJ = json_object_get(manifestJ, "status"); | |||
| if (!statusJ) | |||
| continue; | |||
| std::string status = json_string_value(statusJ); | |||
| if (status != "available") | |||
| continue; | |||
| // Get changelog URL | |||
| json_t* changelogUrlJ = json_object_get(manifestJ, "changelogUrl"); | |||
| if (changelogUrlJ) { | |||
| update.changelogUrl = json_string_value(changelogUrlJ); | |||
| } | |||
| updates.push_back(update); | |||
| } | |||
| // Get module whitelist | |||
| { | |||
| std::string whitelistUrl = API_URL + "/moduleWhitelist"; | |||
| json_t* whitelistResJ = network::requestJson(network::METHOD_GET, whitelistUrl, NULL, cookies); | |||
| if (!whitelistResJ) { | |||
| WARN("Request for module whitelist failed"); | |||
| updateStatus = "Could not query updates"; | |||
| return; | |||
| } | |||
| DEFER({ | |||
| json_decref(whitelistResJ); | |||
| }); | |||
| std::map<std::string, std::set<std::string>> moduleWhitelist; | |||
| json_t* pluginsJ = json_object_get(whitelistResJ, "plugins"); | |||
| // Iterate plugins | |||
| const char* pluginSlug; | |||
| json_t* modulesJ; | |||
| json_object_foreach(pluginsJ, pluginSlug, modulesJ) { | |||
| // Iterate modules in plugin | |||
| size_t moduleIndex; | |||
| json_t* moduleSlugJ; | |||
| json_array_foreach(modulesJ, moduleIndex, moduleSlugJ) { | |||
| std::string moduleSlug = json_string_value(moduleSlugJ); | |||
| // Insert module in whitelist | |||
| moduleWhitelist[pluginSlug].insert(moduleSlug); | |||
| } | |||
| } | |||
| settings::moduleWhitelist = moduleWhitelist; | |||
| } | |||
| updateStatus = ""; | |||
| } | |||
| bool hasUpdates() { | |||
| for (Update& update : updates) { | |||
| if (update.progress < 1.f) | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| static bool isSyncingUpdate = false; | |||
| static bool isSyncingUpdates = false; | |||
| void syncUpdate(Update* update) { | |||
| isSyncingUpdate = true; | |||
| DEFER({ | |||
| isSyncingUpdate = false; | |||
| }); | |||
| std::string downloadUrl = API_URL + "/download"; | |||
| downloadUrl += "?slug=" + network::encodeUrl(update->pluginSlug); | |||
| downloadUrl += "&version=" + network::encodeUrl(update->version); | |||
| downloadUrl += "&arch=" + network::encodeUrl(APP_ARCH); | |||
| network::CookieMap cookies; | |||
| cookies["token"] = settings::token; | |||
| INFO("Downloading plugin %s %s %s", update->pluginSlug.c_str(), update->version.c_str(), APP_ARCH.c_str()); | |||
| // Download zip | |||
| std::string pluginDest = system::join(asset::pluginsPath, update->pluginSlug + ".zip"); | |||
| if (!network::requestDownload(downloadUrl, pluginDest, &update->progress, cookies)) { | |||
| WARN("Plugin %s download was unsuccessful", update->pluginSlug.c_str()); | |||
| return; | |||
| } | |||
| } | |||
| void syncUpdates() { | |||
| isSyncingUpdates = true; | |||
| DEFER({ | |||
| isSyncingUpdates = false; | |||
| }); | |||
| if (settings::token.empty()) | |||
| return; | |||
| for (Update& update : updates) { | |||
| if (update.progress < 1.f) | |||
| syncUpdate(&update); | |||
| } | |||
| restartRequested = true; | |||
| } | |||
| bool isSyncing() { | |||
| return isSyncingUpdate || isSyncingUpdates; | |||
| } | |||
| std::string loginStatus; | |||
| std::vector<Update> updates; | |||
| std::string updateStatus; | |||
| bool restartRequested = false; | |||
| } // namespace library | |||
| } // namespace rack | |||
| @@ -11,7 +11,7 @@ | |||
| #include <windows.h> | |||
| #include <direct.h> | |||
| #else | |||
| #include <dlfcn.h> | |||
| #include <dlfcn.h> // for dlopen | |||
| #endif | |||
| #include <dirent.h> | |||
| @@ -20,14 +20,11 @@ | |||
| #include <plugin.hpp> | |||
| #include <system.hpp> | |||
| #include <network.hpp> | |||
| #include <asset.hpp> | |||
| #include <string.hpp> | |||
| #include <context.hpp> | |||
| #include <app/common.hpp> | |||
| #include <plugin/callbacks.hpp> | |||
| #include <settings.hpp> | |||
| #include <engine/Module.hpp> | |||
| namespace rack { | |||
| @@ -232,14 +229,6 @@ void init() { | |||
| system::unarchiveToFolder(fundamentalSrc.c_str(), asset::pluginsPath.c_str()); | |||
| loadPlugin(fundamentalDir); | |||
| } | |||
| // Sync in a detached thread | |||
| if (!settings::devMode) { | |||
| std::thread t([] { | |||
| queryUpdates(); | |||
| }); | |||
| t.detach(); | |||
| } | |||
| } | |||
| @@ -262,245 +251,6 @@ void destroy() { | |||
| } | |||
| void logIn(const std::string& email, const std::string& password) { | |||
| loginStatus = "Logging in..."; | |||
| json_t* reqJ = json_object(); | |||
| json_object_set(reqJ, "email", json_string(email.c_str())); | |||
| json_object_set(reqJ, "password", json_string(password.c_str())); | |||
| std::string url = API_URL + "/token"; | |||
| json_t* resJ = network::requestJson(network::METHOD_POST, url, reqJ); | |||
| json_decref(reqJ); | |||
| if (!resJ) { | |||
| loginStatus = "No response from server"; | |||
| return; | |||
| } | |||
| DEFER({ | |||
| json_decref(resJ); | |||
| }); | |||
| json_t* errorJ = json_object_get(resJ, "error"); | |||
| if (errorJ) { | |||
| const char* errorStr = json_string_value(errorJ); | |||
| loginStatus = errorStr; | |||
| return; | |||
| } | |||
| json_t* tokenJ = json_object_get(resJ, "token"); | |||
| if (!tokenJ) { | |||
| loginStatus = "No token in response"; | |||
| return; | |||
| } | |||
| const char* tokenStr = json_string_value(tokenJ); | |||
| settings::token = tokenStr; | |||
| loginStatus = ""; | |||
| queryUpdates(); | |||
| } | |||
| void logOut() { | |||
| settings::token = ""; | |||
| updates.clear(); | |||
| } | |||
| bool isLoggedIn() { | |||
| return settings::token != ""; | |||
| } | |||
| void queryUpdates() { | |||
| if (settings::token.empty()) | |||
| return; | |||
| updates.clear(); | |||
| updateStatus = "Querying for updates..."; | |||
| // Get user's plugins list | |||
| std::string pluginsUrl = API_URL + "/plugins"; | |||
| network::CookieMap cookies; | |||
| cookies["token"] = settings::token; | |||
| json_t* pluginsResJ = network::requestJson(network::METHOD_GET, pluginsUrl, NULL, cookies); | |||
| if (!pluginsResJ) { | |||
| WARN("Request for user's plugins failed"); | |||
| updateStatus = "Could not query updates"; | |||
| return; | |||
| } | |||
| DEFER({ | |||
| json_decref(pluginsResJ); | |||
| }); | |||
| json_t* errorJ = json_object_get(pluginsResJ, "error"); | |||
| if (errorJ) { | |||
| WARN("Request for user's plugins returned an error: %s", json_string_value(errorJ)); | |||
| updateStatus = "Could not query updates"; | |||
| return; | |||
| } | |||
| // Get library manifests | |||
| std::string manifestsUrl = API_URL + "/library/manifests"; | |||
| json_t* manifestsReq = json_object(); | |||
| json_object_set(manifestsReq, "version", json_string(API_VERSION.c_str())); | |||
| json_t* manifestsResJ = network::requestJson(network::METHOD_GET, manifestsUrl, manifestsReq); | |||
| json_decref(manifestsReq); | |||
| if (!manifestsResJ) { | |||
| WARN("Request for library manifests failed"); | |||
| updateStatus = "Could not query updates"; | |||
| return; | |||
| } | |||
| DEFER({ | |||
| json_decref(manifestsResJ); | |||
| }); | |||
| json_t* manifestsJ = json_object_get(manifestsResJ, "manifests"); | |||
| json_t* pluginsJ = json_object_get(pluginsResJ, "plugins"); | |||
| size_t pluginIndex; | |||
| json_t* pluginJ; | |||
| json_array_foreach(pluginsJ, pluginIndex, pluginJ) { | |||
| Update update; | |||
| // Get plugin manifest | |||
| update.pluginSlug = json_string_value(pluginJ); | |||
| json_t* manifestJ = json_object_get(manifestsJ, update.pluginSlug.c_str()); | |||
| if (!manifestJ) { | |||
| WARN("VCV account has plugin %s but no manifest was found", update.pluginSlug.c_str()); | |||
| continue; | |||
| } | |||
| // Get plugin name | |||
| json_t* nameJ = json_object_get(manifestJ, "name"); | |||
| if (nameJ) | |||
| update.pluginName = json_string_value(nameJ); | |||
| // Get version | |||
| json_t* versionJ = json_object_get(manifestJ, "version"); | |||
| if (!versionJ) { | |||
| WARN("Plugin %s has no version in manifest", update.pluginSlug.c_str()); | |||
| continue; | |||
| } | |||
| update.version = json_string_value(versionJ); | |||
| // Check if update is needed | |||
| Plugin* p = getPlugin(update.pluginSlug); | |||
| if (p && p->version == update.version) | |||
| continue; | |||
| // Check status | |||
| json_t* statusJ = json_object_get(manifestJ, "status"); | |||
| if (!statusJ) | |||
| continue; | |||
| std::string status = json_string_value(statusJ); | |||
| if (status != "available") | |||
| continue; | |||
| // Get changelog URL | |||
| json_t* changelogUrlJ = json_object_get(manifestJ, "changelogUrl"); | |||
| if (changelogUrlJ) { | |||
| update.changelogUrl = json_string_value(changelogUrlJ); | |||
| } | |||
| updates.push_back(update); | |||
| } | |||
| // Get module whitelist | |||
| { | |||
| std::string whitelistUrl = API_URL + "/moduleWhitelist"; | |||
| json_t* whitelistResJ = network::requestJson(network::METHOD_GET, whitelistUrl, NULL, cookies); | |||
| if (!whitelistResJ) { | |||
| WARN("Request for module whitelist failed"); | |||
| updateStatus = "Could not query updates"; | |||
| return; | |||
| } | |||
| DEFER({ | |||
| json_decref(whitelistResJ); | |||
| }); | |||
| std::map<std::string, std::set<std::string>> moduleWhitelist; | |||
| json_t* pluginsJ = json_object_get(whitelistResJ, "plugins"); | |||
| // Iterate plugins | |||
| const char* pluginSlug; | |||
| json_t* modulesJ; | |||
| json_object_foreach(pluginsJ, pluginSlug, modulesJ) { | |||
| // Iterate modules in plugin | |||
| size_t moduleIndex; | |||
| json_t* moduleSlugJ; | |||
| json_array_foreach(modulesJ, moduleIndex, moduleSlugJ) { | |||
| std::string moduleSlug = json_string_value(moduleSlugJ); | |||
| // Insert module in whitelist | |||
| moduleWhitelist[pluginSlug].insert(moduleSlug); | |||
| } | |||
| } | |||
| settings::moduleWhitelist = moduleWhitelist; | |||
| } | |||
| updateStatus = ""; | |||
| } | |||
| bool hasUpdates() { | |||
| for (Update& update : updates) { | |||
| if (update.progress < 1.f) | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| static bool isSyncingUpdate = false; | |||
| static bool isSyncingUpdates = false; | |||
| void syncUpdate(Update* update) { | |||
| isSyncingUpdate = true; | |||
| DEFER({ | |||
| isSyncingUpdate = false; | |||
| }); | |||
| std::string downloadUrl = API_URL + "/download"; | |||
| downloadUrl += "?slug=" + network::encodeUrl(update->pluginSlug); | |||
| downloadUrl += "&version=" + network::encodeUrl(update->version); | |||
| downloadUrl += "&arch=" + network::encodeUrl(APP_ARCH); | |||
| network::CookieMap cookies; | |||
| cookies["token"] = settings::token; | |||
| INFO("Downloading plugin %s %s %s", update->pluginSlug.c_str(), update->version.c_str(), APP_ARCH.c_str()); | |||
| // Download zip | |||
| std::string pluginDest = system::join(asset::pluginsPath, update->pluginSlug + ".zip"); | |||
| if (!network::requestDownload(downloadUrl, pluginDest, &update->progress, cookies)) { | |||
| WARN("Plugin %s download was unsuccessful", update->pluginSlug.c_str()); | |||
| return; | |||
| } | |||
| } | |||
| void syncUpdates() { | |||
| isSyncingUpdates = true; | |||
| DEFER({ | |||
| isSyncingUpdates = false; | |||
| }); | |||
| if (settings::token.empty()) | |||
| return; | |||
| for (Update& update : updates) { | |||
| if (update.progress < 1.f) | |||
| syncUpdate(&update); | |||
| } | |||
| restartRequested = true; | |||
| } | |||
| bool isSyncing() { | |||
| return isSyncingUpdate || isSyncingUpdates; | |||
| } | |||
| Plugin* getPlugin(const std::string& pluginSlug) { | |||
| for (Plugin* plugin : plugins) { | |||
| if (plugin->slug == pluginSlug) { | |||
| @@ -566,11 +316,6 @@ std::string normalizeSlug(const std::string& slug) { | |||
| std::vector<Plugin*> plugins; | |||
| std::string loginStatus; | |||
| std::vector<Update> updates; | |||
| std::string updateStatus; | |||
| bool restartRequested = false; | |||
| } // namespace plugin | |||
| } // namespace rack | |||
| @@ -1,106 +0,0 @@ | |||
| #include <thread> | |||
| #include <updater.hpp> | |||
| #include <settings.hpp> | |||
| #include <app/common.hpp> | |||
| #include <network.hpp> | |||
| #include <system.hpp> | |||
| #include <context.hpp> | |||
| #include <window.hpp> | |||
| #include <asset.hpp> | |||
| namespace rack { | |||
| namespace updater { | |||
| std::string version; | |||
| std::string changelogUrl; | |||
| float progress = 0.f; | |||
| static std::string downloadUrl; | |||
| static void checkVersion() { | |||
| std::string versionUrl = API_URL + "/version"; | |||
| json_t* resJ = network::requestJson(network::METHOD_GET, versionUrl, NULL); | |||
| if (!resJ) { | |||
| WARN("Request for version failed"); | |||
| return; | |||
| } | |||
| DEFER({ | |||
| json_decref(resJ); | |||
| }); | |||
| json_t* versionJ = json_object_get(resJ, "version"); | |||
| if (versionJ) | |||
| version = json_string_value(versionJ); | |||
| json_t* changelogUrlJ = json_object_get(resJ, "changelogUrl"); | |||
| if (changelogUrlJ) | |||
| changelogUrl = json_string_value(changelogUrlJ); | |||
| json_t* downloadUrlsJ = json_object_get(resJ, "downloadUrls"); | |||
| if (downloadUrlsJ) { | |||
| json_t* downloadUrlJ = json_object_get(downloadUrlsJ, APP_ARCH.c_str()); | |||
| if (downloadUrlJ) | |||
| downloadUrl = json_string_value(downloadUrlJ); | |||
| } | |||
| } | |||
| void init() { | |||
| if (settings::devMode) | |||
| return; | |||
| std::thread t([]() { | |||
| checkVersion(); | |||
| }); | |||
| t.detach(); | |||
| } | |||
| void update() { | |||
| if (downloadUrl == "") | |||
| return; | |||
| // Download update | |||
| // HACK getFilename is only supposed to be used for filesystem paths, not URLs. | |||
| std::string filename = system::getFilename(network::urlPath(downloadUrl)); | |||
| std::string path = asset::user(filename); | |||
| INFO("Downloading update %s to %s", downloadUrl.c_str(), path.c_str()); | |||
| network::requestDownload(downloadUrl, path, &progress); | |||
| #if defined ARCH_WIN | |||
| // Launch the installer | |||
| INFO("Launching update %s", path.c_str()); | |||
| system::runProcessDetached(path); | |||
| #elif defined ARCH_MAC | |||
| std::string cmd; | |||
| // std::string appPath = system::join(asset::userDir, "Rack.app"); | |||
| // cmd = "rm -rf '" + appPath + "'"; | |||
| // std::system(cmd.c_str()); | |||
| // // Unzip app using Apple's unzipper, since Rack's unzipper doesn't handle the metadata stuff correctly. | |||
| // cmd = "unzip -q '" + path + "' -d '" + asset::userDir + "'"; | |||
| // std::system(cmd.c_str()); | |||
| // // Open app in Finder | |||
| // cmd = "open -R '" + appPath + "'"; | |||
| // std::system(cmd.c_str()); | |||
| // Open Archive Utility | |||
| cmd = "open '" + path + "'"; | |||
| std::system(cmd.c_str()); | |||
| #elif defined ARCH_LIN | |||
| system::openFolder(asset::user("")); | |||
| #endif | |||
| APP->window->close(); | |||
| } | |||
| bool isUpdateAvailable() { | |||
| return (version != "") && (version != APP_VERSION); | |||
| } | |||
| } // namespace updater | |||
| } // namespace rack | |||
| @@ -19,7 +19,7 @@ | |||
| #include <ui.hpp> | |||
| #include <system.hpp> | |||
| #include <string.hpp> | |||
| #include <updater.hpp> | |||
| #include <library.hpp> | |||
| #include <network.hpp> | |||
| #include <osdialog.h> | |||
| @@ -168,7 +168,7 @@ int main(int argc, char* argv[]) { | |||
| keyboard::init(); | |||
| gamepad::init(); | |||
| plugin::init(); | |||
| updater::init(); | |||
| library::init(); | |||
| if (!settings::headless) { | |||
| ui::init(); | |||
| windowInit(); | |||