@@ -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 { | namespace rack { | ||||
/** Plugin loader and VCV Library synchronizer | |||||
/** Loads and manages plugins | |||||
*/ | */ | ||||
namespace plugin { | namespace plugin { | ||||
struct Update { | |||||
std::string pluginSlug; | |||||
std::string pluginName; | |||||
std::string version; | |||||
std::string changelogUrl; | |||||
float progress = 0.f; | |||||
}; | |||||
void init(); | void init(); | ||||
void destroy(); | 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); | Plugin* getPlugin(const std::string& pluginSlug); | ||||
Model* getModel(const std::string& pluginSlug, const std::string& modelSlug); | Model* getModel(const std::string& pluginSlug, const std::string& modelSlug); | ||||
/** Creates a Model from a JSON module object. | /** 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::vector<Plugin*> plugins; | ||||
extern std::string loginStatus; | |||||
extern std::vector<Update> updates; | |||||
extern std::string updateStatus; | |||||
extern bool restartRequested; | |||||
} // namespace plugin | } // namespace plugin | ||||
} // namespace rack | } // 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 <system.hpp> | ||||
#include <plugin.hpp> | #include <plugin.hpp> | ||||
#include <patch.hpp> | #include <patch.hpp> | ||||
#include <updater.hpp> | |||||
#include <library.hpp> | |||||
namespace rack { | namespace rack { | ||||
@@ -608,7 +608,7 @@ struct LogInItem : ui::MenuItem { | |||||
std::string email = emailField->text; | std::string email = emailField->text; | ||||
std::string password = passwordField->text; | std::string password = passwordField->text; | ||||
std::thread t([ = ] { | std::thread t([ = ] { | ||||
plugin::logIn(email, password); | |||||
library::logIn(email, password); | |||||
isLoggingIn = false; | isLoggingIn = false; | ||||
}); | }); | ||||
t.detach(); | t.detach(); | ||||
@@ -618,7 +618,7 @@ struct LogInItem : ui::MenuItem { | |||||
void step() override { | void step() override { | ||||
disabled = isLoggingIn; | disabled = isLoggingIn; | ||||
text = "Log in"; | text = "Log in"; | ||||
rightText = plugin::loginStatus; | |||||
rightText = library::loginStatus; | |||||
MenuItem::step(); | MenuItem::step(); | ||||
} | } | ||||
}; | }; | ||||
@@ -626,13 +626,13 @@ struct LogInItem : ui::MenuItem { | |||||
struct SyncItem : ui::MenuItem { | struct SyncItem : ui::MenuItem { | ||||
void step() override { | void step() override { | ||||
disabled = true; | disabled = true; | ||||
if (plugin::updateStatus != "") { | |||||
text = plugin::updateStatus; | |||||
if (library::updateStatus != "") { | |||||
text = library::updateStatus; | |||||
} | } | ||||
else if (plugin::isSyncing()) { | |||||
else if (library::isSyncing()) { | |||||
text = "Updating..."; | text = "Updating..."; | ||||
} | } | ||||
else if (!plugin::hasUpdates()) { | |||||
else if (!library::hasUpdates()) { | |||||
text = "Up-to-date"; | text = "Up-to-date"; | ||||
} | } | ||||
else { | else { | ||||
@@ -644,7 +644,7 @@ struct SyncItem : ui::MenuItem { | |||||
void onAction(const event::Action& e) override { | void onAction(const event::Action& e) override { | ||||
std::thread t([ = ] { | std::thread t([ = ] { | ||||
plugin::syncUpdates(); | |||||
library::syncUpdates(); | |||||
}); | }); | ||||
t.detach(); | t.detach(); | ||||
e.consume(NULL); | e.consume(NULL); | ||||
@@ -652,9 +652,9 @@ struct SyncItem : ui::MenuItem { | |||||
}; | }; | ||||
struct PluginSyncItem : 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; | this->update = update; | ||||
text = update->pluginName; | text = update->pluginName; | ||||
plugin::Plugin* p = plugin::getPlugin(update->pluginSlug); | plugin::Plugin* p = plugin::getPlugin(update->pluginSlug); | ||||
@@ -679,7 +679,7 @@ struct PluginSyncItem : ui::MenuItem { | |||||
} | } | ||||
void step() override { | void step() override { | ||||
disabled = plugin::isSyncing(); | |||||
disabled = library::isSyncing(); | |||||
if (update->progress >= 1) { | if (update->progress >= 1) { | ||||
rightText = CHECKMARK_STRING; | rightText = CHECKMARK_STRING; | ||||
disabled = true; | disabled = true; | ||||
@@ -692,7 +692,7 @@ struct PluginSyncItem : ui::MenuItem { | |||||
void onAction(const event::Action& e) override { | void onAction(const event::Action& e) override { | ||||
std::thread t([ = ] { | std::thread t([ = ] { | ||||
plugin::syncUpdate(update); | |||||
library::syncUpdate(update); | |||||
}); | }); | ||||
t.detach(); | t.detach(); | ||||
e.consume(NULL); | e.consume(NULL); | ||||
@@ -701,7 +701,7 @@ struct PluginSyncItem : ui::MenuItem { | |||||
struct LogOutItem : ui::MenuItem { | struct LogOutItem : ui::MenuItem { | ||||
void onAction(const event::Action& e) override { | void onAction(const event::Action& e) override { | ||||
plugin::logOut(); | |||||
library::logOut(); | |||||
} | } | ||||
}; | }; | ||||
@@ -714,7 +714,7 @@ struct LibraryMenu : ui::Menu { | |||||
void step() override { | void step() override { | ||||
// Refresh menu when appropriate | // Refresh menu when appropriate | ||||
if (!loggedIn && plugin::isLoggedIn()) | |||||
if (!loggedIn && library::isLoggedIn()) | |||||
refresh(); | refresh(); | ||||
Menu::step(); | Menu::step(); | ||||
} | } | ||||
@@ -726,7 +726,7 @@ struct LibraryMenu : ui::Menu { | |||||
if (settings::devMode) { | if (settings::devMode) { | ||||
addChild(createMenuLabel("Disabled in development mode")); | addChild(createMenuLabel("Disabled in development mode")); | ||||
} | } | ||||
else if (!plugin::isLoggedIn()) { | |||||
else if (!library::isLoggedIn()) { | |||||
UrlItem* registerItem = new UrlItem; | UrlItem* registerItem = new UrlItem; | ||||
registerItem->text = "Register VCV account"; | registerItem->text = "Register VCV account"; | ||||
registerItem->url = "https://vcvrack.com/"; | registerItem->url = "https://vcvrack.com/"; | ||||
@@ -765,14 +765,14 @@ struct LibraryMenu : ui::Menu { | |||||
syncItem->text = "Update all"; | syncItem->text = "Update all"; | ||||
addChild(syncItem); | addChild(syncItem); | ||||
if (plugin::hasUpdates()) { | |||||
if (library::hasUpdates()) { | |||||
addChild(new ui::MenuSeparator); | addChild(new ui::MenuSeparator); | ||||
ui::MenuLabel* updatesLabel = new ui::MenuLabel; | ui::MenuLabel* updatesLabel = new ui::MenuLabel; | ||||
updatesLabel->text = "Updates"; | updatesLabel->text = "Updates"; | ||||
addChild(updatesLabel); | addChild(updatesLabel); | ||||
for (plugin::Update& update : plugin::updates) { | |||||
for (library::Update& update : library::updates) { | |||||
PluginSyncItem* updateItem = new PluginSyncItem; | PluginSyncItem* updateItem = new PluginSyncItem; | ||||
updateItem->setUpdate(&update); | updateItem->setUpdate(&update); | ||||
addChild(updateItem); | addChild(updateItem); | ||||
@@ -799,11 +799,11 @@ struct LibraryButton : MenuButton { | |||||
void step() override { | void step() override { | ||||
notification->box.pos = math::Vec(0, 0); | notification->box.pos = math::Vec(0, 0); | ||||
notification->visible = plugin::hasUpdates(); | |||||
notification->visible = library::hasUpdates(); | |||||
// Popup when updates finish downloading | // 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.")) { | 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(); | APP->window->close(); | ||||
} | } | ||||
@@ -823,22 +823,22 @@ struct UpdateItem : ui::MenuItem { | |||||
UrlItem* changelogUrl = new UrlItem; | UrlItem* changelogUrl = new UrlItem; | ||||
changelogUrl->text = "Changelog"; | changelogUrl->text = "Changelog"; | ||||
changelogUrl->url = updater::changelogUrl; | |||||
changelogUrl->url = library::changelogUrl; | |||||
menu->addChild(changelogUrl); | menu->addChild(changelogUrl); | ||||
return menu; | return menu; | ||||
} | } | ||||
void step() override { | 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(); | MenuItem::step(); | ||||
} | } | ||||
void onAction(const event::Action& e) override { | void onAction(const event::Action& e) override { | ||||
std::thread t([ = ] { | std::thread t([ = ] { | ||||
updater::update(); | |||||
library::update(); | |||||
}); | }); | ||||
t.detach(); | t.detach(); | ||||
e.consume(NULL); | e.consume(NULL); | ||||
@@ -859,10 +859,10 @@ struct HelpButton : MenuButton { | |||||
menu->box.pos = getAbsoluteOffset(math::Vec(0, box.size.y)); | menu->box.pos = getAbsoluteOffset(math::Vec(0, box.size.y)); | ||||
menu->box.size.x = box.size.x; | menu->box.size.x = box.size.x; | ||||
if (updater::isUpdateAvailable()) { | |||||
if (library::isUpdateAvailable()) { | |||||
UpdateItem* updateItem = new UpdateItem; | UpdateItem* updateItem = new UpdateItem; | ||||
updateItem->text = "Update " + APP_NAME; | updateItem->text = "Update " + APP_NAME; | ||||
updateItem->rightText = APP_VERSION + " → " + updater::version; | |||||
updateItem->rightText = APP_VERSION + " → " + library::version; | |||||
menu->addChild(updateItem); | menu->addChild(updateItem); | ||||
} | } | ||||
@@ -885,7 +885,7 @@ struct HelpButton : MenuButton { | |||||
void step() override { | void step() override { | ||||
notification->box.pos = math::Vec(0, 0); | notification->box.pos = math::Vec(0, 0); | ||||
notification->visible = updater::isUpdateAvailable(); | |||||
notification->visible = library::isUpdateAvailable(); | |||||
MenuButton::step(); | 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 <windows.h> | ||||
#include <direct.h> | #include <direct.h> | ||||
#else | #else | ||||
#include <dlfcn.h> | |||||
#include <dlfcn.h> // for dlopen | |||||
#endif | #endif | ||||
#include <dirent.h> | #include <dirent.h> | ||||
@@ -20,14 +20,11 @@ | |||||
#include <plugin.hpp> | #include <plugin.hpp> | ||||
#include <system.hpp> | #include <system.hpp> | ||||
#include <network.hpp> | |||||
#include <asset.hpp> | #include <asset.hpp> | ||||
#include <string.hpp> | #include <string.hpp> | ||||
#include <context.hpp> | #include <context.hpp> | ||||
#include <app/common.hpp> | |||||
#include <plugin/callbacks.hpp> | #include <plugin/callbacks.hpp> | ||||
#include <settings.hpp> | #include <settings.hpp> | ||||
#include <engine/Module.hpp> | |||||
namespace rack { | namespace rack { | ||||
@@ -232,14 +229,6 @@ void init() { | |||||
system::unarchiveToFolder(fundamentalSrc.c_str(), asset::pluginsPath.c_str()); | system::unarchiveToFolder(fundamentalSrc.c_str(), asset::pluginsPath.c_str()); | ||||
loadPlugin(fundamentalDir); | 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) { | Plugin* getPlugin(const std::string& pluginSlug) { | ||||
for (Plugin* plugin : plugins) { | for (Plugin* plugin : plugins) { | ||||
if (plugin->slug == pluginSlug) { | if (plugin->slug == pluginSlug) { | ||||
@@ -566,11 +316,6 @@ std::string normalizeSlug(const std::string& slug) { | |||||
std::vector<Plugin*> plugins; | std::vector<Plugin*> plugins; | ||||
std::string loginStatus; | |||||
std::vector<Update> updates; | |||||
std::string updateStatus; | |||||
bool restartRequested = false; | |||||
} // namespace plugin | } // namespace plugin | ||||
} // namespace rack | } // 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 <ui.hpp> | ||||
#include <system.hpp> | #include <system.hpp> | ||||
#include <string.hpp> | #include <string.hpp> | ||||
#include <updater.hpp> | |||||
#include <library.hpp> | |||||
#include <network.hpp> | #include <network.hpp> | ||||
#include <osdialog.h> | #include <osdialog.h> | ||||
@@ -168,7 +168,7 @@ int main(int argc, char* argv[]) { | |||||
keyboard::init(); | keyboard::init(); | ||||
gamepad::init(); | gamepad::init(); | ||||
plugin::init(); | plugin::init(); | ||||
updater::init(); | |||||
library::init(); | |||||
if (!settings::headless) { | if (!settings::headless) { | ||||
ui::init(); | ui::init(); | ||||
windowInit(); | windowInit(); | ||||