#include #include #include #include #include #include #include // for MAXPATHLEN #include #include #include #include #include #if defined(ARCH_WIN) #include #include #define mkdir(_dir, _perms) _mkdir(_dir) #else #include #endif #include #include "plugin.hpp" #include "app.hpp" #include "asset.hpp" #include "util/request.hpp" namespace rack { std::list gPlugins; std::string gToken; static bool isDownloading = false; static float downloadProgress = 0.0; static std::string downloadName; static std::string loginStatus; Plugin::~Plugin() { for (Model *model : models) { delete model; } } void Plugin::addModel(Model *model) { assert(!model->plugin); model->plugin = this; models.push_back(model); } static int loadPlugin(std::string path) { std::string libraryFilename; #if ARCH_LIN libraryFilename = path + "/" + "plugin.so"; #elif ARCH_WIN libraryFilename = path + "/" + "plugin.dll"; #elif ARCH_MAC libraryFilename = path + "/" + "plugin.dylib"; #endif // Load dynamic/shared library #if ARCH_WIN SetErrorMode(SEM_NOOPENFILEERRORBOX | SEM_FAILCRITICALERRORS); HINSTANCE handle = LoadLibrary(libraryFilename.c_str()); SetErrorMode(0); if (!handle) { int error = GetLastError(); warn("Failed to load library %s: %d", libraryFilename.c_str(), error); return -1; } #elif ARCH_LIN || ARCH_MAC void *handle = dlopen(libraryFilename.c_str(), RTLD_NOW); if (!handle) { warn("Failed to load library %s: %s", libraryFilename.c_str(), dlerror()); return -1; } #endif // Call plugin's init() function typedef void (*InitCallback)(Plugin *); InitCallback initCallback; #if ARCH_WIN initCallback = (InitCallback) GetProcAddress(handle, "init"); #elif ARCH_LIN || ARCH_MAC initCallback = (InitCallback) dlsym(handle, "init"); #endif if (!initCallback) { warn("Failed to read init() symbol in %s", libraryFilename.c_str()); return -2; } // Construct and initialize Plugin instance Plugin *plugin = new Plugin(); plugin->path = path; plugin->handle = handle; initCallback(plugin); // Reject plugin if slug already exists for (Plugin *p : gPlugins) { if (plugin->slug == p->slug) { warn("Plugin \"%s\" is already loaded, not attempting to load it again"); // TODO // Fix memory leak with `plugin` here return -1; } } // Add plugin to list gPlugins.push_back(plugin); info("Loaded plugin %s", libraryFilename.c_str()); return 0; } static void loadPlugins(std::string path) { DIR *dir = opendir(path.c_str()); if (dir) { struct dirent *d; while ((d = readdir(dir))) { if (d->d_name[0] == '.') continue; loadPlugin(path + "/" + d->d_name); } closedir(dir); } } //////////////////// // plugin helpers //////////////////// static int extractZipHandle(zip_t *za, const char *dir) { int err = 0; for (int i = 0; i < zip_get_num_entries(za, 0); i++) { zip_stat_t zs; err = zip_stat_index(za, i, 0, &zs); if (err) return err; int nameLen = strlen(zs.name); char path[MAXPATHLEN]; snprintf(path, sizeof(path), "%s/%s", dir, zs.name); if (zs.name[nameLen - 1] == '/') { err = mkdir(path, 0755); if (err && errno != EEXIST) return err; } else { zip_file_t *zf = zip_fopen_index(za, i, 0); if (!zf) return 1; FILE *outFile = fopen(path, "wb"); if (!outFile) continue; while (1) { char buffer[4096]; int len = zip_fread(zf, buffer, sizeof(buffer)); if (len <= 0) break; fwrite(buffer, 1, len, outFile); } err = zip_fclose(zf); if (err) return err; fclose(outFile); } } return 0; } static int extractZip(const char *filename, const char *dir) { int err = 0; zip_t *za = zip_open(filename, 0, &err); if (!za) return 1; if (!err) { err = extractZipHandle(za, dir); } zip_close(za); return err; } static bool syncPlugin(json_t *pluginJ, bool dryRun) { json_t *slugJ = json_object_get(pluginJ, "slug"); if (!slugJ) return false; std::string slug = json_string_value(slugJ); // Get community version std::string version; json_t *versionJ = json_object_get(pluginJ, "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; } } json_t *nameJ = json_object_get(pluginJ, "name"); if (!nameJ) return false; std::string name = json_string_value(nameJ); 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); } } 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 false; } // If plugin is not loaded, download the zip file to /plugins downloadName = name; downloadProgress = 0.0; // Download zip std::string pluginsDir = assetLocal("plugins"); std::string pluginPath = pluginsDir + "/" + slug; std::string zipPath = pluginPath + ".zip"; bool success = requestDownload(download, zipPath, &downloadProgress); if (!success) { warn("Plugin %s download was unsuccessful"); return false; } 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 false; } } // Unzip file int err = extractZip(zipPath.c_str(), pluginsDir.c_str()); if (!err) { // Delete zip remove(zipPath.c_str()); // Load plugin // loadPlugin(pluginPath); } downloadName = ""; return true; } bool pluginSync(bool dryRun) { if (gToken.empty()) return false; bool available = false; if (!dryRun) { isDownloading = true; downloadProgress = 0.0; downloadName = "Updating plugins..."; } json_t *resJ = NULL; json_t *communityResJ = NULL; try { // Download plugin slugs 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())); resJ = requestJson(METHOD_GET, gApiHost + "/plugins", reqJ); json_decref(reqJ); if (!resJ) throw std::runtime_error("No response from server"); json_t *errorJ = json_object_get(resJ, "error"); if (errorJ) throw std::runtime_error(json_string_value(errorJ)); // Download community plugins communityResJ = requestJson(METHOD_GET, gApiHost + "/community/plugins", NULL); if (!communityResJ) throw std::runtime_error("No response from server"); json_t *communityErrorJ = json_object_get(communityResJ, "error"); if (communityErrorJ) throw std::runtime_error(json_string_value(communityErrorJ)); // Check each plugin in list of plugin slugs json_t *pluginSlugsJ = json_object_get(resJ, "plugins"); json_t *communityPluginsJ = json_object_get(communityResJ, "plugins"); size_t index; json_t *pluginSlugJ; json_array_foreach(pluginSlugsJ, index, pluginSlugJ) { std::string slug = json_string_value(pluginSlugJ); // Search for plugin slug in community size_t communityIndex; json_t *communityPluginJ = NULL; json_array_foreach(communityPluginsJ, communityIndex, communityPluginJ) { json_t *communitySlugJ = json_object_get(communityPluginJ, "slug"); if (!communitySlugJ) continue; std::string communitySlug = json_string_value(communitySlugJ); if (slug == communitySlug) break; } // Sync plugin if (syncPlugin(communityPluginJ, dryRun)) { available = true; } else { warn("Plugin %s not found in community", slug.c_str()); } } } catch (std::runtime_error &e) { warn("Plugin sync error: %s", e.what()); } if (communityResJ) json_decref(communityResJ); if (resJ) json_decref(resJ); if (!dryRun) { isDownloading = false; } return available; } //////////////////// // plugin API //////////////////// void pluginInit() { tagsInit(); // TODO // If `/plugins/Fundamental` doesn't exist, unzip global Fundamental.zip package into `/plugins` // TODO // Find all ZIP packages in `/plugins` and unzip them. // Display error if failure // Load core // This function is defined in core.cpp Plugin *coreManufacturer = new Plugin(); init(coreManufacturer); gPlugins.push_back(coreManufacturer); // Load plugins from local directory std::string localPlugins = assetLocal("plugins"); mkdir(localPlugins.c_str(), 0755); info("Loading plugins from %s", localPlugins.c_str()); loadPlugins(localPlugins); } void pluginDestroy() { for (Plugin *plugin : gPlugins) { // Free library handle #if defined(ARCH_WIN) if (plugin->handle) FreeLibrary((HINSTANCE)plugin->handle); #elif defined(ARCH_LIN) || defined(ARCH_MAC) if (plugin->handle) dlclose(plugin->handle); #endif // For some reason this segfaults. // It might be best to let them leak anyway, because "crash on exit" issues would occur with badly-written plugins. // delete plugin; } gPlugins.clear(); } void pluginLogIn(std::string email, std::string password) { 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())); json_t *resJ = requestJson(METHOD_POST, gApiHost + "/token", reqJ); json_decref(reqJ); if (resJ) { json_t *errorJ = json_object_get(resJ, "error"); if (errorJ) { const char *errorStr = json_string_value(errorJ); loginStatus = errorStr; } else { json_t *tokenJ = json_object_get(resJ, "token"); if (tokenJ) { const char *tokenStr = json_string_value(tokenJ); gToken = tokenStr; loginStatus = ""; } } json_decref(resJ); } } void pluginLogOut() { gToken = ""; } void pluginCancelDownload() { // TODO } bool pluginIsLoggedIn() { return gToken != ""; } bool pluginIsDownloading() { return isDownloading; } float pluginGetDownloadProgress() { return downloadProgress; } std::string pluginGetDownloadName() { return downloadName; } std::string pluginGetLoginStatus() { return loginStatus; } Plugin *pluginGetPlugin(std::string pluginSlug) { for (Plugin *plugin : gPlugins) { if (plugin->slug == pluginSlug) { return plugin; } } return NULL; } Model *pluginGetModel(std::string pluginSlug, std::string modelSlug) { Plugin *plugin = pluginGetPlugin(pluginSlug); if (plugin) { for (Model *model : plugin->models) { if (model->slug == modelSlug) { return model; } } } return NULL; } } // namespace rack