Browse Source

Update plugin manager to new online Rack plugin manager API

tags/v0.5.1
Andrew Belt 6 years ago
parent
commit
dc91b5d403
7 changed files with 240 additions and 71 deletions
  1. +1
    -1
      Makefile
  2. +2
    -1
      include/plugin.hpp
  3. +1
    -0
      include/util/request.hpp
  4. +1
    -0
      src/app.cpp
  5. +52
    -12
      src/app/PluginManagerWidget.cpp
  6. +150
    -57
      src/plugin.cpp
  7. +33
    -0
      src/util/request.cpp

+ 1
- 1
Makefile View File

@@ -14,7 +14,7 @@ ifeq ($(ARCH), lin)
LDFLAGS += -rdynamic \
-lpthread -lGL -ldl \
$(shell pkg-config --libs gtk+-2.0) \
-Ldep/lib -lGLEW -lglfw -ljansson -lsamplerate -lcurl -lzip -lrtaudio -lrtmidi
-Ldep/lib -lGLEW -lglfw -ljansson -lsamplerate -lcurl -lzip -lrtaudio -lrtmidi -lcrypto
TARGET = Rack
endif



+ 2
- 1
include/plugin.hpp View File

@@ -107,7 +107,8 @@ void pluginInit();
void pluginDestroy();
void pluginLogIn(std::string email, std::string password);
void pluginLogOut();
void pluginRefresh();
/** Returns whether a new plugin is available, and downloads it unless doing a dry run */
bool pluginSync(bool dryRun);
void pluginCancelDownload();
bool pluginIsLoggedIn();
bool pluginIsDownloading();


+ 1
- 0
include/util/request.hpp View File

@@ -19,5 +19,6 @@ json_t *requestJson(RequestMethod method, std::string url, json_t *dataJ);
/** Returns the filename, blank if unsuccessful */
bool requestDownload(std::string url, std::string filename, float *progress);
std::string requestEscape(std::string s);
std::string requestSHA256File(std::string filename);

} // namespace rack

+ 1
- 0
src/app.cpp View File

@@ -11,6 +11,7 @@ std::string gApplicationVersion =
"";
#endif
std::string gApiHost = "https://api.vcvrack.com";
// std::string gApiHost = "http://localhost:8081";

RackWidget *gRackWidget = NULL;
Toolbar *gToolbar = NULL;


+ 52
- 12
src/app/PluginManagerWidget.cpp View File

@@ -1,11 +1,57 @@
#include <thread>
#include "app.hpp"
#include "plugin.hpp"
#include "gui.hpp"
#include "../ext/osdialog/osdialog.h"


namespace rack {


struct SyncButton : Button {
bool checked = false;
bool available = false;
bool completed = false;

void step() override {
if (!checked) {
std::thread t([this]() {
if (pluginSync(true))
available = true;
});
t.detach();
checked = true;
}
if (completed) {
if (osdialog_message(OSDIALOG_INFO, OSDIALOG_OK_CANCEL, "All plugins have been updated. Close Rack and re-launch it to load new updates.")) {
guiClose();
}
completed = false;
}
}
void draw(NVGcontext *vg) override {
Button::draw(vg);
if (available) {
// Notification circle
nvgBeginPath(vg);
nvgCircle(vg, 3, 3, 4.0);
nvgFillColor(vg, nvgRGBf(1.0, 0.0, 0.0));
nvgFill(vg);
nvgStrokeColor(vg, nvgRGBf(0.5, 0.0, 0.0));
nvgStroke(vg);
}
}
void onAction(EventAction &e) override {
available = false;
std::thread t([this]() {
if (pluginSync(false))
completed = true;
});
t.detach();
}
};


PluginManagerWidget::PluginManagerWidget() {
box.size.y = BND_WIDGET_HEIGHT;
float margin = 5;
@@ -91,19 +137,13 @@ PluginManagerWidget::PluginManagerWidget() {
manageWidget->addChild(manageButton);
pos.x += manageButton->box.size.x;

struct RefreshButton : Button {
void onAction(EventAction &e) override {
std::thread t(pluginRefresh);
t.detach();
}
};
pos.x += margin;
Button *refreshButton = new RefreshButton();
refreshButton->box.pos = pos;
refreshButton->box.size.x = 125;
refreshButton->text = "Refresh plugins";
manageWidget->addChild(refreshButton);
pos.x += refreshButton->box.size.x;
Button *syncButton = new SyncButton();
syncButton->box.pos = pos;
syncButton->box.size.x = 125;
syncButton->text = "Update plugins";
manageWidget->addChild(syncButton);
pos.x += syncButton->box.size.x;

struct LogOutButton : Button {
void onAction(EventAction &e) override {


+ 150
- 57
src/plugin.cpp View File

@@ -7,11 +7,12 @@
#include <sys/stat.h>
#include <sys/param.h> // for MAXPATHLEN
#include <fcntl.h>
#include <thread>

#include <zip.h>
#include <jansson.h>

#if ARCH_WIN
#if defined(ARCH_WIN)
#include <windows.h>
#include <direct.h>
#define mkdir(_dir, _perms) _mkdir(_dir)
@@ -225,35 +226,55 @@ static int extractZip(const char *filename, const char *dir) {
return err;
}

static void refreshPurchase(json_t *pluginJ) {
static void syncPlugin(json_t *pluginJ) {
json_t *slugJ = json_object_get(pluginJ, "slug");
if (!slugJ) return;
std::string slug = json_string_value(slugJ);
info("Syncing plugin %s", slug.c_str());

json_t *nameJ = json_object_get(pluginJ, "name");
if (!nameJ) return;
std::string name = json_string_value(nameJ);

json_t *versionJ = json_object_get(pluginJ, "version");
if (!versionJ) return;
std::string version = json_string_value(versionJ);

// Check whether the plugin is already loaded
for (Plugin *plugin : gPlugins) {
if (plugin->slug == slug && plugin->version == version) {
return;
std::string download;
std::string sha256;

json_t *downloadsJ = json_object_get(pluginJ, "downloads");
if (downloadsJ) {
#if defined(ARCH_WIN)
#define DOWNLOADS_ARCH "win"
#elif defined(ARCH_MAC)
#define DOWNLOADS_ARCH "mac"
#elif defined(ARCH_LIN)
#define DOWNLOADS_ARCH "lin"
#endif
json_t *archJ = json_object_get(downloadsJ, DOWNLOADS_ARCH);
if (archJ) {
// Get download URL
json_t *downloadJ = json_object_get(archJ, "download");
if (downloadJ)
download = json_string_value(downloadJ);
// Get SHA256 hash
json_t *sha256J = json_object_get(archJ, "sha256");
if (sha256J)
sha256 = json_string_value(sha256J);
}
}

// Append token and version to download URL
std::string url = gApiHost;
url += "/download";
url += "?product=";
url += slug;
url += "&version=";
url += requestEscape(gApplicationVersion);
url += "&token=";
url += requestEscape(gToken);
json_t *productIdJ = json_object_get(pluginJ, "productId");
if (productIdJ) {
download = gApiHost;
download += "/download";
download += "?slug=";
download += slug;
download += "&token=";
download += requestEscape(gToken);
}

if (download.empty()) {
warn("Could not get download URL for plugin %s", slug.c_str());
return;
}

// If plugin is not loaded, download the zip file to /plugins
downloadName = name;
@@ -263,21 +284,127 @@ static void refreshPurchase(json_t *pluginJ) {
std::string pluginsDir = assetLocal("plugins");
std::string pluginPath = pluginsDir + "/" + slug;
std::string zipPath = pluginPath + ".zip";
bool success = requestDownload(url, zipPath, &downloadProgress);
bool success = requestDownload(download, zipPath, &downloadProgress);
if (success) {
if (!sha256.empty()) {
// Check SHA256 hash
std::string actualSha256 = requestSHA256File(zipPath);
if (actualSha256 != sha256) {
warn("Plugin %s does not match expected SHA256 checksum", slug.c_str());
return;
}
}

// Unzip file
int err = extractZip(zipPath.c_str(), pluginsDir.c_str());
if (!err) {
// Delete zip
remove(zipPath.c_str());
// Load plugin
loadPlugin(pluginPath);
// loadPlugin(pluginPath);
}
}

downloadName = "";
}

static bool trySyncPlugin(json_t *pluginJ, json_t *communityPluginsJ, bool dryRun) {
std::string slug = json_string_value(pluginJ);

// Find community plugin
size_t communityIndex;
json_t *communityPluginJ = NULL;
json_array_foreach(communityPluginsJ, communityIndex, communityPluginJ) {
json_t *communitySlugJ = json_object_get(communityPluginJ, "slug");
if (communitySlugJ) {
std::string communitySlug = json_string_value(communitySlugJ);
if (slug == communitySlug)
break;
}
}
if (communityIndex == json_array_size(communityPluginsJ)) {
warn("Plugin sync error: %s not found in community", slug.c_str());
return false;
}

// Get community version
std::string version;
json_t *versionJ = json_object_get(communityPluginJ, "version");
if (versionJ) {
version = json_string_value(versionJ);
}

// Check whether we already have a plugin with the same slug and version
for (Plugin *plugin : gPlugins) {
if (plugin->slug == slug) {
// plugin->version might be blank, so adding a version of the manifest will update the plugin
if (plugin->version == version)
return false;
}
}

if (!dryRun)
syncPlugin(communityPluginJ);
return true;
}

bool pluginSync(bool dryRun) {
if (gToken.empty())
return false;

bool available = false;

// Download my plugins
json_t *reqJ = json_object();
json_object_set(reqJ, "version", json_string(gApplicationVersion.c_str()));
json_object_set(reqJ, "token", json_string(gToken.c_str()));
json_t *resJ = requestJson(METHOD_GET, gApiHost + "/plugins", reqJ);
json_decref(reqJ);

// Download community plugins
json_t *communityResJ = requestJson(METHOD_GET, gApiHost + "/community/plugins", NULL);

if (!dryRun) {
isDownloading = true;
downloadProgress = 0.0;
downloadName = "";
}

if (resJ && communityResJ) {
json_t *errorJ = json_object_get(resJ, "error");
json_t *communityErrorJ = json_object_get(resJ, "error");
if (errorJ) {
warn("Plugin sync error: %s", json_string_value(errorJ));
}
else if (communityErrorJ) {
warn("Plugin sync error: %s", json_string_value(communityErrorJ));
}
else {
// Check each plugin in list of my plugins
json_t *pluginsJ = json_object_get(resJ, "plugins");
json_t *communityPluginsJ = json_object_get(communityResJ, "plugins");
size_t index;
json_t *pluginJ;
json_array_foreach(pluginsJ, index, pluginJ) {
if (trySyncPlugin(pluginJ, communityPluginsJ, dryRun))
available = true;
}
}
}

if (resJ)
json_decref(resJ);

if (communityResJ)
json_decref(communityResJ);

if (!dryRun) {
isDownloading = false;
}

return available;
}

////////////////////
// plugin API
////////////////////
@@ -306,10 +433,10 @@ void pluginInit() {
void pluginDestroy() {
for (Plugin *plugin : gPlugins) {
// Free library handle
#if ARCH_WIN
#if defined(ARCH_WIN)
if (plugin->handle)
FreeLibrary((HINSTANCE)plugin->handle);
#elif ARCH_LIN || ARCH_MAC
#elif defined(ARCH_LIN) || defined(ARCH_MAC)
if (plugin->handle)
dlclose(plugin->handle);
#endif
@@ -350,40 +477,6 @@ void pluginLogOut() {
gToken = "";
}

void pluginRefresh() {
if (gToken.empty())
return;

isDownloading = true;
downloadProgress = 0.0;
downloadName = "";

json_t *reqJ = json_object();
json_object_set(reqJ, "version", json_string(gApplicationVersion.c_str()));
json_object_set(reqJ, "token", json_string(gToken.c_str()));
json_t *resJ = requestJson(METHOD_GET, gApiHost + "/purchases", reqJ);
json_decref(reqJ);

if (resJ) {
json_t *errorJ = json_object_get(resJ, "error");
if (errorJ) {
const char *errorStr = json_string_value(errorJ);
warn("Plugin refresh error: %s", errorStr);
}
else {
json_t *purchasesJ = json_object_get(resJ, "purchases");
size_t index;
json_t *purchaseJ;
json_array_foreach(purchasesJ, index, purchaseJ) {
refreshPurchase(purchaseJ);
}
}
json_decref(resJ);
}

isDownloading = false;
}

void pluginCancelDownload() {
// TODO
}


+ 33
- 0
src/util/request.cpp View File

@@ -4,6 +4,7 @@
#include <stdio.h>
#include <string.h>
#include <curl/curl.h>
#include <openssl/sha.h>


namespace rack {
@@ -130,6 +131,7 @@ bool requestDownload(std::string url, std::string filename, float *progress) {
curl_easy_setopt(curl, CURLOPT_WRITEDATA, file);
curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, xferInfoCallback);
curl_easy_setopt(curl, CURLOPT_XFERINFODATA, progress);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true);

info("Downloading %s", url.c_str());
CURLcode res = curl_easy_perform(curl);
@@ -153,5 +155,36 @@ std::string requestEscape(std::string s) {
return ret;
}

std::string requestSHA256File(std::string filename) {
FILE *f = fopen(filename.c_str(), "rb");
if (!f)
return "";

uint8_t hash[SHA256_DIGEST_LENGTH];
SHA256_CTX sha256;
SHA256_Init(&sha256);
const int bufferLen = 1 << 15;
uint8_t *buffer = new uint8_t[bufferLen];
int len = 0;
while ((len = fread(buffer, 1, bufferLen, f))) {
SHA256_Update(&sha256, buffer, len);
}
SHA256_Final(hash, &sha256);
delete[] buffer;
fclose(f);

// Convert binary hash to hex
char hashHex[64];
const char hexTable[] = "0123456789abcdef";
for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
uint8_t h = hash[i];
hashHex[2*i + 0] = hexTable[h >> 4];
hashHex[2*i + 1] = hexTable[h & 0x0f];
}

std::string str(hashHex, sizeof(hashHex));
return str;
}


} // namespace rack

Loading…
Cancel
Save