commit ac0ec38375acae2a0fcde1d65786bb7d4461c8bc
parent 7e1628eac5bcd700bdd5bc8cf679bf472b8ca4ee
Author: cfillion <cfillion@users.noreply.github.com>
Date: Mon, 27 Feb 2017 16:59:21 -0500
Merge branch 'archiving'
Diffstat:
38 files changed, 1395 insertions(+), 504 deletions(-)
diff --git a/Tupfile b/Tupfile
@@ -5,12 +5,17 @@ TINYXML := $(WDL)/tinyxml
WDLSOURCE += $(TINYXML)/tinyxml.cpp $(TINYXML)/tinystr.cpp
WDLSOURCE += $(TINYXML)/tinyxmlparser.cpp $(TINYXML)/tinyxmlerror.cpp
+ZLIB := $(WDL)/zlib
+WDLSOURCE += $(ZLIB)/zip.c $(ZLIB)/unzip.c $(ZLIB)/inflate.c $(ZLIB)/deflate.c
+WDLSOURCE += $(ZLIB)/zutil.c $(ZLIB)/crc32.c $(ZLIB)/adler32.c $(ZLIB)/ioapi.c
+WDLSOURCE += $(ZLIB)/trees.c $(ZLIB)/inftrees.c $(ZLIB)/inffast.c
+
include @(TUP_PLATFORM).tup
-: foreach src/*.cpp | $(BUILDDEPS) |> !build |> build/%B.o
+: foreach src/*.cpp | $(BUILDDEPS) |> !build $(SRCFLAGS) |> build/%B.o
: foreach $(WDLSOURCE) |> !build $(WDLFLAGS) |> build/wdl_%B.o
: build/*.o | $(LINKDEPS) |> !link $(SOFLAGS) |> $(SOTARGET)
-: foreach test/*.cpp |> !build -Isrc |> build/test/%B.o
-: foreach test/helper/*.cpp |> !build -Isrc |> build/test/helper_%B.o
+: foreach test/*.cpp |> !build -Isrc $(SRCFLAGS) |> build/test/%B.o
+: foreach test/helper/*.cpp |> !build -Isrc $(SRCFLAGS) |> build/test/helper_%B.o
: build/*.o build/test/*.o | $(LINKDEPS) |> !link $(TSFLAGS) |> $(TSTARGET)
diff --git a/macosx.tup b/macosx.tup
@@ -1,4 +1,4 @@
-CXX := c++
+CXX := clang
REAPACK_FILE = reaper_reapack@(SUFFIX).dylib
@@ -6,20 +6,21 @@ CXXFLAGS := -Wall -Wextra -Werror
CXXFLAGS += -Wno-unused-parameter -Wno-missing-field-initializers
CXXFLAGS += -Wno-unused-function -Wno-unused-private-field -Wno-missing-braces
CXXFLAGS += -fdiagnostics-color -fstack-protector-strong -fvisibility=hidden
-CXXFLAGS += -pipe -fPIC -O2 -std=c++14
+CXXFLAGS += -pipe -fPIC -O2
+CXXFLAGS += -arch @(ARCH) -mmacosx-version-min=10.7
CXXFLAGS += -Ivendor -Ivendor/WDL -Ivendor/WDL/WDL -Ivendor/WDL/WDL/swell
CXXFLAGS += -DWDL_NO_DEFINE_MINMAX -DSWELL_APP_PREFIX=SWELL_REAPACK
CXXFLAGS += -DREAPACK_FILE=\"$(REAPACK_FILE)\"
-CXXFLAGS += -arch @(ARCH) -mmacosx-version-min=10.7 -stdlib=libc++
-WDLFLAGS := -std=c++98 -w
+SRCFLAGS := -std=c++14 -stdlib=libc++
+WDLFLAGS := -w
SWELL := $(WDL)/swell
WDLSOURCE += $(SWELL)/swell.cpp $(SWELL)/swell-ini.cpp $(SWELL)/swell-miscdlg.mm
WDLSOURCE += $(SWELL)/swell-gdi.mm $(SWELL)/swell-kb.mm $(SWELL)/swell-menu.mm
WDLSOURCE += $(SWELL)/swell-misc.mm $(SWELL)/swell-dlg.mm $(SWELL)/swell-wnd.mm
-LDFLAGS := -framework Cocoa -framework Carbon -lcurl -lsqlite3
+LDFLAGS := -framework Cocoa -framework Carbon -lcurl -lsqlite3 -lc++
SOFLAGS := -dynamiclib
SOTARGET := bin/$(REAPACK_FILE)
@@ -33,4 +34,4 @@ BUILDDEPS := src/resource.rc_mac_menu src/resource.rc_mac_dlg
: src/resource.rc |> php $(SWELL)/mac_resgen.php %f |> $(BUILDDEPS)
# build Objective-C files
-: foreach src/*.mm | $(BUILDDEPS) |> !build |> build/%B_mm.o
+: foreach src/*.mm | $(BUILDDEPS) |> !build $(SRCFLAGS) |> build/%B_mm.o
diff --git a/src/about.cpp b/src/about.cpp
@@ -18,6 +18,7 @@
#include "about.hpp"
#include "browser.hpp"
+#include "config.hpp"
#include "encoding.hpp"
#include "errors.hpp"
#include "filesystem.hpp"
diff --git a/src/archive.cpp b/src/archive.cpp
@@ -0,0 +1,377 @@
+/* ReaPack: Package manager for REAPER
+ * Copyright (C) 2015-2017 Christian Fillion
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "archive.hpp"
+
+#include "config.hpp"
+#include "errors.hpp"
+#include "filesystem.hpp"
+#include "index.hpp"
+#include "path.hpp"
+#include "reapack.hpp"
+#include "transaction.hpp"
+
+#include <boost/format.hpp>
+#include <fstream>
+#include <iomanip>
+#include <sstream>
+
+#include <zlib/zip.h>
+#include <zlib/unzip.h>
+#include <zlib/ioapi.h>
+
+using namespace boost;
+using namespace std;
+
+static const Path ARCHIVE_TOC = Path("toc");
+static const size_t BUFFER_SIZE = 4096;
+
+#ifdef _WIN32
+static void *wide_fopen(voidpf, const void *filename, int mode)
+{
+ const wchar_t *fopen_mode = nullptr;
+
+ if((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER) == ZLIB_FILEFUNC_MODE_READ)
+ fopen_mode = L"rb";
+ else if(mode & ZLIB_FILEFUNC_MODE_EXISTING)
+ fopen_mode = L"r+b";
+ else if(mode & ZLIB_FILEFUNC_MODE_CREATE)
+ fopen_mode = L"wb";
+
+ FILE *file = nullptr;
+
+ if(filename && fopen_mode)
+ _wfopen_s(&file, static_cast<const wchar_t *>(filename), fopen_mode);
+
+ return file;
+}
+#endif
+
+struct ImportArchive {
+ void importRemote(const string &);
+ void importPackage(const string &);
+
+ ArchiveReaderPtr m_reader;
+ RemoteList *m_remotes;
+ Transaction *m_tx;
+ IndexPtr m_lastIndex;
+};
+
+void Archive::import(const auto_string &path, ReaPack *reapack)
+{
+ ImportArchive state{make_shared<ArchiveReader>(path), &reapack->config()->remotes};
+
+ stringstream toc;
+ if(const int err = state.m_reader->extractFile(ARCHIVE_TOC, toc))
+ throw reapack_error(format("Cannot locate the table of contents (%d)") % err);
+
+ // starting import, do not abort process (eg. by throwing) at this point
+ if(!(state.m_tx = reapack->setupTransaction()))
+ return;
+
+ string line;
+ while(getline(toc, line)) {
+ if(line.size() <= 5) // 5 is the length of the line type prefix
+ continue;
+
+ const string &data = line.substr(5);
+
+ try {
+ switch(line[0]) {
+ case 'R':
+ state.importRemote(data);
+ break;
+ case 'P':
+ state.importPackage(data);
+ break;
+ default:
+ throw reapack_error(format("Unknown token '%s' (skipping)")
+ % line.substr(0, 4));
+ }
+ }
+ catch(const reapack_error &e) {
+ state.m_tx->receipt()->addError({e.what(), from_autostring(path)});
+ }
+ }
+
+ reapack->config()->write();
+ state.m_tx->runTasks();
+}
+
+void ImportArchive::importRemote(const string &data)
+{
+ m_lastIndex = nullptr; // clear the previous repository
+ Remote remote = Remote::fromString(data);
+
+ if(const int err = m_reader->extractFile(Index::pathFor(remote.name()))) {
+ throw reapack_error(format("Failed to extract index of %s (%d)")
+ % remote.name() % err);
+ }
+
+ const Remote &original = m_remotes->get(remote.name());
+ if(original.isProtected()) {
+ remote.setUrl(original.url());
+ remote.protect();
+ }
+
+ m_remotes->add(remote);
+ m_lastIndex = Index::load(remote.name());
+}
+
+void ImportArchive::importPackage(const string &data)
+{
+ // don't report an error if the index isn't loaded assuming we already
+ // did when failing to import the repository above
+ if(!m_lastIndex)
+ return;
+
+ string categoryName, packageName, versionName;
+ bool pinned;
+
+ istringstream stream(data);
+ stream
+ >> quoted(categoryName) >> quoted(packageName) >> quoted(versionName)
+ >> pinned;
+
+ const Package *pkg = m_lastIndex->find(categoryName, packageName);
+ const Version *ver = pkg ? pkg->findVersion(versionName) : nullptr;
+
+ if(!ver) {
+ throw reapack_error(format("%s/%s/%s v%s cannot be found or is"
+ " incompatible with your operating system.")
+ % m_lastIndex->name() % categoryName % packageName % versionName);
+ }
+
+ m_tx->install(ver, pinned, m_reader);
+}
+
+ArchiveReader::ArchiveReader(const auto_string &path)
+{
+ zlib_filefunc64_def filefunc;
+ fill_fopen64_filefunc(&filefunc);
+#ifdef _WIN32
+ filefunc.zopen64_file = wide_fopen;
+#endif
+
+ m_zip = unzOpen2_64(reinterpret_cast<const char *>(path.c_str()), &filefunc);
+
+ if(!m_zip)
+ throw reapack_error(FS::lastError().c_str());
+}
+
+ArchiveReader::~ArchiveReader()
+{
+ unzClose(m_zip);
+}
+
+int ArchiveReader::extractFile(const Path &path)
+{
+ ofstream stream;
+
+ if(FS::open(stream, path))
+ return extractFile(path, stream);
+ else
+ throw reapack_error(format("%s: %s") % path.join() % FS::lastError());
+}
+
+int ArchiveReader::extractFile(const Path &path, ostream &stream) noexcept
+{
+ int status = unzLocateFile(m_zip, path.join('/').c_str(), false);
+ if(status != UNZ_OK)
+ return status;
+
+ status = unzOpenCurrentFile(m_zip);
+ if(status != UNZ_OK)
+ return status;
+
+ string buffer(BUFFER_SIZE, 0);
+
+ const auto readChunk = [&] {
+ return unzReadCurrentFile(m_zip, &buffer[0], (int)buffer.size());
+ };
+
+ while(const int len = readChunk()) {
+ if(len < 0)
+ return len; // read error
+
+ stream.write(&buffer[0], len);
+ }
+
+ return unzCloseCurrentFile(m_zip);
+}
+
+FileExtractor::FileExtractor(const Path &target, const ArchiveReaderPtr &reader)
+ : m_path(target), m_reader(reader)
+{
+ setSummary("Extracting %s: " + target.join());
+}
+
+void FileExtractor::run(DownloadContext *)
+{
+ if(aborted()) {
+ finish(Aborted, {"cancelled", m_path.target().join()});
+ return;
+ }
+
+ ThreadNotifier::get()->notify({this, Running});
+
+ ofstream stream;
+ if(!FS::open(stream, m_path.temp())) {
+ finish(Failure, {FS::lastError(), m_path.temp().join()});
+ return;
+ }
+
+ const int error = m_reader->extractFile(m_path.target(), stream);
+ stream.close();
+
+ if(error) {
+ const format &msg = format("Failed to extract file (%d)") % error;
+ finish(Failure, {msg.str(), m_path.target().join()});
+ }
+ else
+ finish(Success);
+}
+
+size_t Archive::create(const auto_string &path, ThreadPool *pool, ReaPack *reapack)
+{
+ size_t count = 0;
+ vector<ThreadTask *> jobs;
+
+ stringstream toc;
+ Registry reg(Path::prefixRoot(Path::REGISTRY));
+
+ ArchiveWriterPtr writer = make_shared<ArchiveWriter>(path);
+
+ for(const Remote &remote : reapack->config()->remotes.getEnabled()) {
+ bool addedRemote = false;
+
+ for(const Registry::Entry &entry : reg.getEntries(remote.name())) {
+ ++count;
+
+ if(!addedRemote) {
+ toc << "REPO " << remote.toString() << '\n';
+ jobs.push_back(new FileCompressor(Index::pathFor(remote.name()), writer));
+ addedRemote = true;
+ }
+
+ toc << "PACK "
+ << quoted(entry.category) << '\x20'
+ << quoted(entry.package) << '\x20'
+ << quoted(entry.version.toString()) << '\x20'
+ << entry.pinned << '\n'
+ ;
+
+ for(const Registry::File &file : reg.getFiles(entry))
+ jobs.push_back(new FileCompressor(file.path, writer));
+ }
+ }
+
+ writer->addFile(ARCHIVE_TOC, toc);
+
+ // start after we've written the table of contents in the main thread
+ for(ThreadTask *job : jobs)
+ pool->push(job);
+
+ return count;
+}
+
+ArchiveWriter::ArchiveWriter(const auto_string &path)
+{
+ zlib_filefunc64_def filefunc;
+ fill_fopen64_filefunc(&filefunc);
+#ifdef _WIN32
+ filefunc.zopen64_file = wide_fopen;
+#endif
+
+ m_zip = zipOpen2_64(reinterpret_cast<const char *>(path.c_str()),
+ APPEND_STATUS_CREATE, nullptr, &filefunc);
+
+ if(!m_zip)
+ throw reapack_error(FS::lastError().c_str());
+}
+
+ArchiveWriter::~ArchiveWriter()
+{
+ zipClose(m_zip, nullptr);
+}
+
+int ArchiveWriter::addFile(const Path &path)
+{
+ ifstream stream;
+
+ if(FS::open(stream, path))
+ return addFile(path, stream);
+ else
+ throw reapack_error(format("%s: %s") % path.join() % FS::lastError());
+}
+
+int ArchiveWriter::addFile(const Path &path, istream &stream) noexcept
+{
+ const int status = zipOpenNewFileInZip(m_zip, path.join('/').c_str(), nullptr,
+ nullptr, 0, nullptr, 0, nullptr, Z_DEFLATED, Z_DEFAULT_COMPRESSION);
+
+ if(status != ZIP_OK)
+ return status;
+
+ string buffer(BUFFER_SIZE, 0);
+
+ const auto readChunk = [&] {
+ stream.read(&buffer[0], buffer.size());
+ return (int)stream.gcount();
+ };
+
+ while(const int len = readChunk()) {
+ if(len < 0)
+ return len; // write error
+
+ zipWriteInFileInZip(m_zip, &buffer[0], len);
+ }
+
+ return zipCloseFileInZip(m_zip);
+}
+
+FileCompressor::FileCompressor(const Path &target, const ArchiveWriterPtr &writer)
+ : m_path(target), m_writer(writer)
+{
+ setSummary("Compressing %s: " + target.join());
+}
+
+void FileCompressor::run(DownloadContext *)
+{
+ if(aborted()) {
+ finish(Aborted, {"cancelled", m_path.join()});
+ return;
+ }
+
+ ThreadNotifier::get()->notify({this, Running});
+
+ ifstream stream;
+ if(!FS::open(stream, m_path)) {
+ finish(Failure, {FS::lastError(), m_path.join()});
+ return;
+ }
+
+ const int error = m_writer->addFile(m_path, stream);
+ stream.close();
+
+ if(error) {
+ const format &msg = format("Failed to compress file (%d)") % error;
+ finish(Failure, {msg.str(), m_path.join()});
+ }
+ else
+ finish(Success);
+}
diff --git a/src/archive.hpp b/src/archive.hpp
@@ -0,0 +1,86 @@
+/* ReaPack: Package manager for REAPER
+ * Copyright (C) 2015-2017 Christian Fillion
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef REAPACK_ARCHIVE_HPP
+#define REAPACK_ARCHIVE_HPP
+
+#include "encoding.hpp"
+#include "path.hpp"
+#include "thread.hpp"
+
+class ReaPack;
+class ThreadPool;
+
+typedef void *zipFile;
+
+namespace Archive {
+ void import(const auto_string &path, ReaPack *);
+ size_t create(const auto_string &path, ThreadPool *pool, ReaPack *);
+};
+
+class ArchiveReader {
+public:
+ ArchiveReader(const auto_string &path);
+ ~ArchiveReader();
+ int extractFile(const Path &);
+ int extractFile(const Path &, std::ostream &) noexcept;
+
+private:
+ zipFile m_zip;
+};
+
+typedef std::shared_ptr<ArchiveReader> ArchiveReaderPtr;
+
+class ArchiveWriter {
+public:
+ ArchiveWriter(const auto_string &path);
+ ~ArchiveWriter();
+ int addFile(const Path &fn);
+ int addFile(const Path &fn, std::istream &) noexcept;
+
+private:
+ zipFile m_zip;
+};
+
+typedef std::shared_ptr<ArchiveWriter> ArchiveWriterPtr;
+
+class FileExtractor : public ThreadTask {
+public:
+ FileExtractor(const Path &target, const ArchiveReaderPtr &);
+ const TempPath &path() const { return m_path; }
+
+ bool concurrent() const override { return false; }
+ void run(DownloadContext *) override;
+
+private:
+ TempPath m_path;
+ ArchiveReaderPtr m_reader;
+};
+
+class FileCompressor : public ThreadTask {
+public:
+ FileCompressor(const Path &target, const ArchiveWriterPtr &);
+
+ bool concurrent() const override { return false; }
+ void run(DownloadContext *) override;
+
+private:
+ Path m_path;
+ ArchiveWriterPtr m_writer;
+};
+
+#endif
diff --git a/src/download.cpp b/src/download.cpp
@@ -17,6 +17,7 @@
#include "download.hpp"
+#include "filesystem.hpp"
#include "reapack.hpp"
#include <boost/format.hpp>
@@ -28,13 +29,11 @@ using namespace std;
static const int DOWNLOAD_TIMEOUT = 15;
// to set the amount of concurrent downloads, change the size of
-// the m_pool member in DownloadQueue
+// the m_pool member in ThreadPool (thread.hpp)
static CURLSH *g_curlShare = nullptr;
static WDL_Mutex g_curlMutex;
-DownloadNotifier *DownloadNotifier::s_instance = nullptr;
-
static void LockCurlMutex(CURL *, curl_lock_data, curl_lock_access, void *)
{
g_curlMutex.Enter();
@@ -45,7 +44,7 @@ static void UnlockCurlMutex(CURL *, curl_lock_data, curl_lock_access, void *)
g_curlMutex.Leave();
}
-void Download::Init()
+void DownloadContext::GlobalInit()
{
curl_global_init(CURL_GLOBAL_DEFAULT);
@@ -59,17 +58,41 @@ void Download::Init()
curl_share_setopt(g_curlShare, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION);
}
-void Download::Cleanup()
+void DownloadContext::GlobalCleanup()
{
curl_share_cleanup(g_curlShare);
curl_global_cleanup();
}
+DownloadContext::DownloadContext()
+{
+ m_curl = curl_easy_init();
+
+ const auto userAgent = format("ReaPack/%s REAPER/%s")
+ % ReaPack::VERSION % GetAppVersion();
+
+ curl_easy_setopt(m_curl, CURLOPT_USERAGENT, userAgent.str().c_str());
+ curl_easy_setopt(m_curl, CURLOPT_LOW_SPEED_LIMIT, 1);
+ curl_easy_setopt(m_curl, CURLOPT_LOW_SPEED_TIME, DOWNLOAD_TIMEOUT);
+ curl_easy_setopt(m_curl, CURLOPT_CONNECTTIMEOUT, DOWNLOAD_TIMEOUT);
+ curl_easy_setopt(m_curl, CURLOPT_FOLLOWLOCATION, true);
+ curl_easy_setopt(m_curl, CURLOPT_MAXREDIRS, 5);
+ curl_easy_setopt(m_curl, CURLOPT_ACCEPT_ENCODING, "");
+ curl_easy_setopt(m_curl, CURLOPT_FAILONERROR, true);
+ curl_easy_setopt(m_curl, CURLOPT_SHARE, g_curlShare);
+ curl_easy_setopt(m_curl, CURLOPT_NOPROGRESS, false);
+}
+
+DownloadContext::~DownloadContext()
+{
+ curl_easy_cleanup(m_curl);
+}
+
size_t Download::WriteData(char *data, size_t rawsize, size_t nmemb, void *ptr)
{
const size_t size = rawsize * nmemb;
- static_cast<string *>(ptr)->append(data, size);
+ static_cast<ostream *>(ptr)->write(data, size);
return size;
}
@@ -77,253 +100,103 @@ size_t Download::WriteData(char *data, size_t rawsize, size_t nmemb, void *ptr)
int Download::UpdateProgress(void *ptr, const double, const double,
const double, const double)
{
- return static_cast<Download *>(ptr)->m_abort;
+ return static_cast<Download *>(ptr)->aborted();
}
-Download::Download(const string &name, const string &url,
- const NetworkOpts &opts, const int flags)
- : m_name(name), m_url(url), m_opts(opts), m_flags(flags),
- m_state(Idle), m_abort(false)
+Download::Download(const string &url, const NetworkOpts &opts, const int flags)
+ : m_url(url), m_opts(opts), m_flags(flags)
{
- DownloadNotifier::get()->start();
}
-Download::~Download()
+void Download::setName(const string &name)
{
- DownloadNotifier::get()->stop();
-}
-
-void Download::setState(const State state)
-{
- m_state = state;
-
- switch(state) {
- case Idle:
- break;
- case Running:
- m_onStart();
- break;
- case Success:
- case Failure:
- case Aborted:
- m_onFinish();
- m_cleanupHandler();
- break;
- }
+ setSummary("Downloading %s: " + name);
}
void Download::start()
{
- DownloadThread *thread = new DownloadThread;
+ WorkerThread *thread = new WorkerThread;
thread->push(this);
onFinish([thread] { delete thread; });
}
-void Download::exec(CURL *curl)
+void Download::run(DownloadContext *ctx)
{
- const auto finish = [&](const State state, const string &contents) {
- m_contents = contents;
-
- DownloadNotifier::get()->notify({this, state});
- };
-
- if(m_abort) {
- finish(Aborted, "cancelled");
+ if(aborted()) {
+ finish(Aborted, {"cancelled", m_url});
return;
}
- DownloadNotifier::get()->notify({this, Running});
+ ThreadNotifier::get()->notify({this, Running});
- string contents;
+ ostream *stream = openStream();
+ if(!stream)
+ return;
- curl_easy_setopt(curl, CURLOPT_URL, m_url.c_str());
- curl_easy_setopt(curl, CURLOPT_PROXY, m_opts.proxy.c_str());
- curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, m_opts.verifyPeer);
+ curl_easy_setopt(ctx->m_curl, CURLOPT_URL, m_url.c_str());
+ curl_easy_setopt(ctx->m_curl, CURLOPT_PROXY, m_opts.proxy.c_str());
+ curl_easy_setopt(ctx->m_curl, CURLOPT_SSL_VERIFYPEER, m_opts.verifyPeer);
- curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, UpdateProgress);
- curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, this);
+ curl_easy_setopt(ctx->m_curl, CURLOPT_PROGRESSFUNCTION, UpdateProgress);
+ curl_easy_setopt(ctx->m_curl, CURLOPT_PROGRESSDATA, this);
- curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteData);
- curl_easy_setopt(curl, CURLOPT_WRITEDATA, &contents);
+ curl_easy_setopt(ctx->m_curl, CURLOPT_WRITEFUNCTION, WriteData);
+ curl_easy_setopt(ctx->m_curl, CURLOPT_WRITEDATA, stream);
curl_slist *headers = nullptr;
if(has(Download::NoCacheFlag))
headers = curl_slist_append(headers, "Cache-Control: no-cache");
- curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+ curl_easy_setopt(ctx->m_curl, CURLOPT_HTTPHEADER, headers);
char errbuf[CURL_ERROR_SIZE] = "No details";
- curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf);
+ curl_easy_setopt(ctx->m_curl, CURLOPT_ERRORBUFFER, errbuf);
- const CURLcode res = curl_easy_perform(curl);
+ const CURLcode res = curl_easy_perform(ctx->m_curl);
+ closeStream();
- if(m_abort)
- finish(Aborted, "aborted by user");
+ if(aborted())
+ finish(Aborted, {"aborted", m_url});
else if(res != CURLE_OK) {
const auto err = format("%s (%d): %s") % curl_easy_strerror(res) % res % errbuf;
- finish(Failure, err.str());
+ finish(Failure, {err.str(), m_url});
}
else
- finish(Success, contents);
+ finish(Success);
curl_slist_free_all(headers);
}
-DownloadThread::DownloadThread() : m_exit(false)
+MemoryDownload::MemoryDownload(const string &url, const NetworkOpts &opts, int flags)
+ : Download(url, opts, flags)
{
- m_wake = CreateEvent(nullptr, true, false, AUTO_STR("WakeEvent"));
- m_thread = CreateThread(nullptr, 0, thread, (void *)this, 0, nullptr);
+ setName(url);
}
-DownloadThread::~DownloadThread()
+FileDownload::FileDownload(const Path &target, const string &url,
+ const NetworkOpts &opts, int flags)
+ : Download(url, opts, flags), m_path(target)
{
- // remove all pending downloads then wake the thread to make it exit
- m_exit = true;
- SetEvent(m_wake);
-
- WaitForSingleObject(m_thread, INFINITE);
-
- CloseHandle(m_wake);
- CloseHandle(m_thread);
-}
-
-DWORD WINAPI DownloadThread::thread(void *ptr)
-{
- DownloadThread *thread = static_cast<DownloadThread *>(ptr);
- CURL *curl = curl_easy_init();
-
- const auto userAgent = format("ReaPack/%s REAPER/%s")
- % ReaPack::VERSION % GetAppVersion();
-
- curl_easy_setopt(curl, CURLOPT_USERAGENT, userAgent.str().c_str());
- curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 1);
- curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, DOWNLOAD_TIMEOUT);
- curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, DOWNLOAD_TIMEOUT);
- curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true);
- curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5);
- curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");
- curl_easy_setopt(curl, CURLOPT_FAILONERROR, true);
- curl_easy_setopt(curl, CURLOPT_SHARE, g_curlShare);
- curl_easy_setopt(curl, CURLOPT_NOPROGRESS, false);
-
- while(!thread->m_exit) {
- while(Download *dl = thread->nextDownload())
- dl->exec(curl);
-
- ResetEvent(thread->m_wake);
- WaitForSingleObject(thread->m_wake, INFINITE);
- }
-
- curl_easy_cleanup(curl);
-
- return 0;
+ setName(target.join());
}
-Download *DownloadThread::nextDownload()
+bool FileDownload::save()
{
- WDL_MutexLock lock(&m_mutex);
-
- if(m_queue.empty())
- return nullptr;
-
- Download *dl = m_queue.front();
- m_queue.pop();
- return dl;
-}
-
-void DownloadThread::push(Download *dl)
-{
- WDL_MutexLock lock(&m_mutex);
-
- m_queue.push(dl);
- SetEvent(m_wake);
-}
-
-DownloadQueue::~DownloadQueue()
-{
- // don't emit DownloadQueue::onAbort from the destructor
- // which is most likely to cause a crash
- m_onAbort.disconnect_all_slots();
-
- abort();
-}
-
-void DownloadQueue::push(Download *dl)
-{
- m_onPush(dl);
- m_running.insert(dl);
-
- dl->onFinish([=] {
- m_running.erase(dl);
-
- // call m_onDone() only after every onFinish slots ran
- if(m_running.empty())
- m_onDone();
- });
-
- dl->setCleanupHandler([=] { delete dl; });
-
- auto &thread = m_pool[m_running.size() % m_pool.size()];
- if(!thread)
- thread = make_unique<DownloadThread>();
-
- thread->push(dl);
-}
-
-void DownloadQueue::abort()
-{
- for(Download *dl : m_running)
- dl->abort();
-
- m_onAbort();
-}
-
-DownloadNotifier *DownloadNotifier::get()
-{
- if(!s_instance)
- s_instance = new DownloadNotifier;
-
- return s_instance;
-}
-
-void DownloadNotifier::start()
-{
- if(!m_active++)
- plugin_register("timer", (void *)tick);
-}
-
-void DownloadNotifier::stop()
-{
- --m_active;
-}
-
-void DownloadNotifier::notify(const Notification ¬if)
-{
- WDL_MutexLock lock(&m_mutex);
-
- m_queue.push(notif);
+ if(state() == Success)
+ return FS::rename(m_path);
+ else
+ return FS::remove(m_path.temp());
}
-void DownloadNotifier::tick()
+ostream *FileDownload::openStream()
{
- DownloadNotifier *instance = DownloadNotifier::get();
- instance->processQueue();
+ if(FS::open(m_stream, m_path.temp()))
+ return &m_stream;
- // doing this in stop() would cause a use after free of m_mutex in processQueue
- if(!instance->m_active) {
- plugin_register("-timer", (void *)tick);
-
- delete s_instance;
- s_instance = nullptr;
- }
+ finish(Failure, {FS::lastError(), m_path.temp().join()});
+ return nullptr;
}
-void DownloadNotifier::processQueue()
+void FileDownload::closeStream()
{
- WDL_MutexLock lock(&m_mutex);
-
- while(!m_queue.empty()) {
- const Notification ¬if = m_queue.front();
- notif.first->setState(notif.second);
- m_queue.pop();
- }
+ m_stream.close();
}
diff --git a/src/download.hpp b/src/download.hpp
@@ -19,150 +19,81 @@
#define REAPACK_DOWNLOAD_HPP
#include "config.hpp"
+#include "path.hpp"
+#include "thread.hpp"
-#include <array>
-#include <atomic>
-#include <functional>
-#include <queue>
-#include <string>
-#include <unordered_set>
-
-#include <boost/signals2.hpp>
-#include <WDL/mutex.h>
+#include <fstream>
+#include <sstream>
#include <curl/curl.h>
-#include <reaper_plugin.h>
-class Download {
-public:
- typedef boost::signals2::signal<void ()> VoidSignal;
- typedef std::function<void ()> CleanupHandler;
-
- enum State {
- Idle,
- Running,
- Success,
- Failure,
- Aborted,
- };
+struct DownloadContext {
+ static void GlobalInit();
+ static void GlobalCleanup();
+
+ DownloadContext();
+ ~DownloadContext();
+ CURL *m_curl;
+};
+
+class Download : public ThreadTask {
+public:
enum Flag {
NoCacheFlag = 1<<0,
};
- static void Init();
- static void Cleanup();
-
- Download(const std::string &name, const std::string &url,
- const NetworkOpts &, int flags = 0);
- ~Download();
+ Download(const std::string &url, const NetworkOpts &, int flags = 0);
- const std::string &name() const { return m_name; }
+ void setName(const std::string &);
const std::string &url() const { return m_url; }
- const NetworkOpts &options() const { return m_opts; }
- bool has(Flag f) const { return (m_flags & f) != 0; }
-
- void setState(State);
- State state() const { return m_state; }
- const std::string &contents() { return m_contents; }
-
- void onStart(const VoidSignal::slot_type &slot) { m_onStart.connect(slot); }
- void onFinish(const VoidSignal::slot_type &slot) { m_onFinish.connect(slot); }
- void setCleanupHandler(const CleanupHandler &cb) { m_cleanupHandler = cb; }
-
void start();
- void abort() { m_abort = true; }
- void exec(CURL *);
+ bool concurrent() const override { return true; }
+ void run(DownloadContext *) override;
+
+private:
+ virtual std::ostream *openStream() = 0;
+ virtual void closeStream() {}
private:
+ bool has(Flag f) const { return (m_flags & f) != 0; }
static size_t WriteData(char *, size_t, size_t, void *);
static int UpdateProgress(void *, double, double, double, double);
- std::string m_name;
std::string m_url;
NetworkOpts m_opts;
int m_flags;
-
- State m_state;
- std::atomic_bool m_abort;
- std::string m_contents;
-
- VoidSignal m_onStart;
- VoidSignal m_onFinish;
- CleanupHandler m_cleanupHandler;
-};
-
-class DownloadThread {
-public:
- DownloadThread();
- ~DownloadThread();
-
- void push(Download *);
- void clear();
-
-private:
- static DWORD WINAPI thread(void *);
- Download *nextDownload();
-
- HANDLE m_wake;
- HANDLE m_thread;
- std::atomic_bool m_exit;
- WDL_Mutex m_mutex;
- std::queue<Download *> m_queue;
};
-class DownloadQueue {
+class MemoryDownload : public Download {
public:
- typedef boost::signals2::signal<void ()> VoidSignal;
- typedef boost::signals2::signal<void (Download *)> DownloadSignal;
+ MemoryDownload(const std::string &url, const NetworkOpts &, int flags = 0);
- DownloadQueue() {}
- DownloadQueue(const DownloadQueue &) = delete;
- ~DownloadQueue();
+ std::string contents() const { return m_stream.str(); }
- void push(Download *);
- void abort();
-
- bool idle() const { return m_running.empty(); }
-
- void onPush(const DownloadSignal::slot_type &slot) { m_onPush.connect(slot); }
- void onAbort(const VoidSignal::slot_type &slot) { m_onAbort.connect(slot); }
- void onDone(const VoidSignal::slot_type &slot) { m_onDone.connect(slot); }
+protected:
+ std::ostream *openStream() override { return &m_stream; }
private:
- std::array<std::unique_ptr<DownloadThread>, 3> m_pool;
- std::unordered_set<Download *> m_running;
-
- DownloadSignal m_onPush;
- VoidSignal m_onAbort;
- VoidSignal m_onDone;
+ std::stringstream m_stream;
};
-// This singleton class receives state change notifications from a
-// worker thread and applies them in the main thread
-class DownloadNotifier {
- typedef std::pair<Download *, Download::State> Notification;
-
+class FileDownload : public Download {
public:
- static DownloadNotifier *get();
+ FileDownload(const Path &target, const std::string &url,
+ const NetworkOpts &, int flags = 0);
- void start();
- void stop();
+ const TempPath &path() const { return m_path; }
+ bool save();
- void notify(const Notification &);
+protected:
+ std::ostream *openStream() override;
+ void closeStream() override;
private:
- static DownloadNotifier *s_instance;
- static void tick();
-
- DownloadNotifier() : m_active(0) {}
- ~DownloadNotifier() = default;
- void processQueue();
-
- WDL_Mutex m_mutex;
- size_t m_active;
- std::queue<Notification> m_queue;
+ TempPath m_path;
+ std::ofstream m_stream;
};
#endif
diff --git a/src/errors.hpp b/src/errors.hpp
@@ -27,4 +27,9 @@ public:
reapack_error(const boost::format &f) : std::runtime_error(f.str()) {}
};
+struct ErrorInfo {
+ std::string message;
+ std::string context;
+};
+
#endif
diff --git a/src/filedialog.cpp b/src/filedialog.cpp
@@ -0,0 +1,78 @@
+/* ReaPack: Package manager for REAPER
+ * Copyright (C) 2015-2017 Christian Fillion
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "filedialog.hpp"
+
+#include "path.hpp"
+
+#ifndef _WIN32
+#include <swell.h>
+#endif
+
+auto_string FileDialog::getOpenFileName(HWND parent, HINSTANCE instance,
+ const auto_char *title, const Path &initialDir,
+ const auto_char *filters, const auto_char *defaultExt)
+{
+#ifdef _WIN32
+ const auto_string &dirPath = make_autostring(initialDir.join());
+ auto_char path[4096] = {};
+
+ OPENFILENAME of{sizeof(OPENFILENAME), parent, instance};
+ of.lpstrFilter = filters;
+ of.lpstrFile = path;
+ of.nMaxFile = auto_size(path);
+ of.lpstrInitialDir = dirPath.c_str();
+ of.lpstrTitle = title;
+ of.Flags = OFN_HIDEREADONLY | OFN_EXPLORER | OFN_FILEMUSTEXIST;
+ of.lpstrDefExt = defaultExt;
+
+ return GetOpenFileName(&of) ? path : auto_string();
+#else
+ char *path = BrowseForFiles(title, initialDir.join().c_str(),
+ nullptr, false, filters);
+ return path ? path : auto_string();
+#endif
+}
+
+auto_string FileDialog::getSaveFileName(HWND parent, HINSTANCE instance,
+ const auto_char *title, const Path &initialDir,
+ const auto_char *filters, const auto_char *defaultExt)
+{
+#ifdef _WIN32
+ const auto_string &dirPath = make_autostring(initialDir.join());
+ auto_char path[4096] = {};
+
+ OPENFILENAME of{sizeof(OPENFILENAME), parent, instance};
+ of.lpstrFilter = filters;
+ of.lpstrFile = path;
+ of.nMaxFile = auto_size(path);
+ of.lpstrInitialDir = dirPath.c_str();
+ of.lpstrTitle = title;
+ of.Flags = OFN_HIDEREADONLY | OFN_EXPLORER | OFN_OVERWRITEPROMPT;
+ of.lpstrDefExt = defaultExt;
+
+ return GetSaveFileName(&of) ? path : auto_string();
+#else
+ char path[4096] = {};
+
+ if(BrowseForSaveFile(title, initialDir.join().c_str(),
+ nullptr, filters, path, sizeof(path)))
+ return path;
+ else
+ return {};
+#endif
+}
diff --git a/src/filedialog.hpp b/src/filedialog.hpp
@@ -0,0 +1,38 @@
+/* ReaPack: Package manager for REAPER
+ * Copyright (C) 2015-2017 Christian Fillion
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef REAPACK_FILEBROWSE_HPP
+#define REAPACK_FILEBROWSE_HPP
+
+#include "encoding.hpp"
+
+#ifdef _WIN32
+#include <windows.h>
+#else
+#include <swell-types.h>
+#endif
+
+class Path;
+
+namespace FileDialog {
+ auto_string getOpenFileName(HWND, HINSTANCE, const auto_char *title,
+ const Path &directory, const auto_char *filters, const auto_char *defext);
+ auto_string getSaveFileName(HWND, HINSTANCE, const auto_char *title,
+ const Path &directory, const auto_char *filters, const auto_char *defext);
+};
+
+#endif
diff --git a/src/filesystem.cpp b/src/filesystem.cpp
@@ -46,14 +46,26 @@ FILE *FS::open(const Path &path)
#endif
}
-bool FS::write(const Path &path, const string &contents)
+bool FS::open(ifstream &stream, const Path &path)
+{
+ const Path &fullPath = Path::prefixRoot(path);
+ stream.open(make_autostring(fullPath.join()), ios_base::binary);
+ return stream.good();
+}
+
+bool FS::open(ofstream &stream, const Path &path)
{
mkdir(path.dirname());
const Path &fullPath = Path::prefixRoot(path);
- ofstream file(make_autostring(fullPath.join()), ios_base::binary);
+ stream.open(make_autostring(fullPath.join()), ios_base::binary);
+ return stream.good();
+}
- if(!file)
+bool FS::write(const Path &path, const string &contents)
+{
+ ofstream file;
+ if(!open(file, path))
return false;
file << contents;
@@ -62,6 +74,15 @@ bool FS::write(const Path &path, const string &contents)
return true;
}
+bool FS::rename(const TempPath &path)
+{
+#ifdef _WIN32
+ remove(path.target());
+#endif
+
+ return rename(path.temp(), path.target());
+}
+
bool FS::rename(const Path &from, const Path &to)
{
const string &fullFrom = Path::prefixRoot(from).join();
diff --git a/src/filesystem.hpp b/src/filesystem.hpp
@@ -21,10 +21,14 @@
#include <string>
class Path;
+class TempPath;
namespace FS {
FILE *open(const Path &);
+ bool open(std::ifstream &, const Path &);
+ bool open(std::ofstream &, const Path &);
bool write(const Path &, const std::string &);
+ bool rename(const TempPath &);
bool rename(const Path &, const Path &);
bool remove(const Path &);
bool removeRecursive(const Path &);
diff --git a/src/import.cpp b/src/import.cpp
@@ -93,11 +93,12 @@ void Import::fetch()
setWaiting(true);
- Download *dl = m_download = new Download({}, url, m_reapack->config()->network);
+ const auto &opts = m_reapack->config()->network;
+ MemoryDownload *dl = m_download = new MemoryDownload(url, opts);
dl->onFinish([=] {
- const Download::State state = dl->state();
- if(state == Download::Aborted) {
+ const ThreadTask::State state = dl->state();
+ if(state == ThreadTask::Aborted) {
// at this point `this` is deleted, so there is nothing else
// we can do without crashing
return;
@@ -105,8 +106,8 @@ void Import::fetch()
setWaiting(false);
- if(state != Download::Success) {
- const string msg = "Download failed: " + dl->contents();
+ if(state != ThreadTask::Success) {
+ const string msg = "Download failed: " + dl->error().message;
MessageBox(handle(), make_autostring(msg).c_str(), TITLE, MB_OK);
SetFocus(m_url);
return;
@@ -117,7 +118,7 @@ void Import::fetch()
dl->setCleanupHandler([=] {
// if we are still alive
- if(dl->state() != Download::Aborted)
+ if(dl->state() != ThreadTask::Aborted)
m_download = nullptr;
delete dl;
diff --git a/src/import.hpp b/src/import.hpp
@@ -24,7 +24,7 @@
#include <string>
-class Download;
+class MemoryDownload;
class ReaPack;
class Remote;
@@ -45,7 +45,7 @@ private:
void setWaiting(bool);
ReaPack *m_reapack;
- Download *m_download;
+ MemoryDownload *m_download;
short m_fakePos;
HWND m_url;
diff --git a/src/index.cpp b/src/index.cpp
@@ -82,7 +82,7 @@ IndexPtr Index::load(const string &name, const char *data)
return IndexPtr(ri);
}
-Download *Index::fetch(const Remote &remote,
+FileDownload *Index::fetch(const Remote &remote,
const bool stale, const NetworkOpts &opts)
{
time_t mtime = 0, now = time(nullptr);
@@ -94,7 +94,10 @@ Download *Index::fetch(const Remote &remote,
return nullptr;
}
- return new Download(remote.name(), remote.url(), opts, Download::NoCacheFlag);
+ const Path &path = pathFor(remote.name());
+ auto fd = new FileDownload(path, remote.url(), opts, Download::NoCacheFlag);
+ fd->setName(remote.name());
+ return fd;
}
Index::Index(const string &name)
diff --git a/src/index.hpp b/src/index.hpp
@@ -28,8 +28,8 @@
#include "package.hpp"
#include "source.hpp"
-class Download;
class Index;
+class FileDownload;
class Path;
class Remote;
class TiXmlElement;
@@ -41,7 +41,7 @@ class Index : public std::enable_shared_from_this<const Index> {
public:
static Path pathFor(const std::string &name);
static IndexPtr load(const std::string &name, const char *data = nullptr);
- static Download *fetch(const Remote &, bool stale, const NetworkOpts &);
+ static FileDownload *fetch(const Remote &, bool stale, const NetworkOpts &);
Index(const std::string &name);
~Index();
diff --git a/src/manager.cpp b/src/manager.cpp
@@ -18,22 +18,33 @@
#include "manager.hpp"
#include "about.hpp"
+#include "archive.hpp"
#include "config.hpp"
#include "encoding.hpp"
+#include "errors.hpp"
+#include "filedialog.hpp"
#include "import.hpp"
#include "menu.hpp"
+#include "progress.hpp"
#include "reapack.hpp"
#include "remote.hpp"
#include "resource.hpp"
#include "transaction.hpp"
+static const auto_char *ARCHIVE_FILTER =
+ AUTO_STR("ReaPack Offline Archive (*.ReaPackArchive)\0*.ReaPackArchive\0");
+static const auto_char *ARCHIVE_EXT = AUTO_STR("ReaPackArchive");
+
using namespace std;
-enum { ACTION_ENABLE = 80, ACTION_DISABLE, ACTION_UNINSTALL, ACTION_ABOUT,
- ACTION_REFRESH, ACTION_COPYURL, ACTION_SELECT, ACTION_UNSELECT,
- ACTION_AUTOINSTALL_GLOBAL, ACTION_AUTOINSTALL_OFF, ACTION_AUTOINSTALL_ON,
- ACTION_AUTOINSTALL, ACTION_BLEEDINGEDGE, ACTION_PROMPTOBSOLETE,
- ACTION_NETCONFIG, ACTION_RESETCONFIG };
+enum {
+ ACTION_ENABLE = 80, ACTION_DISABLE, ACTION_UNINSTALL, ACTION_ABOUT,
+ ACTION_REFRESH, ACTION_COPYURL, ACTION_SELECT, ACTION_UNSELECT,
+ ACTION_AUTOINSTALL_GLOBAL, ACTION_AUTOINSTALL_OFF, ACTION_AUTOINSTALL_ON,
+ ACTION_AUTOINSTALL, ACTION_BLEEDINGEDGE, ACTION_PROMPTOBSOLETE,
+ ACTION_NETCONFIG, ACTION_RESETCONFIG, ACTION_IMPORT_REPO,
+ ACTION_IMPORT_ARCHIVE, ACTION_EXPORT_ARCHIVE
+};
Manager::Manager(ReaPack *reapack)
: Dialog(IDD_CONFIG_DIALOG),
@@ -88,7 +99,7 @@ void Manager::onCommand(const int id, int)
{
switch(id) {
case IDC_IMPORT:
- import();
+ importExport();
break;
case IDC_BROWSE:
launchBrowser();
@@ -120,6 +131,15 @@ void Manager::onCommand(const int id, int)
case ACTION_UNINSTALL:
uninstall();
break;
+ case ACTION_IMPORT_REPO:
+ importRepo();
+ break;
+ case ACTION_IMPORT_ARCHIVE:
+ importArchive();
+ break;
+ case ACTION_EXPORT_ARCHIVE:
+ exportArchive();
+ break;
case ACTION_AUTOINSTALL:
toggle(m_autoInstall, m_config->install.autoInstall);
break;
@@ -446,7 +466,18 @@ void Manager::copyUrl()
setClipboard(values);
}
-bool Manager::import()
+void Manager::importExport()
+{
+ Menu menu;
+ menu.addAction(AUTO_STR("Import a &repository..."), ACTION_IMPORT_REPO);
+ menu.addSeparator();
+ menu.addAction(AUTO_STR("Import offline archive..."), ACTION_IMPORT_ARCHIVE);
+ menu.addAction(AUTO_STR("&Export offline archive..."), ACTION_EXPORT_ARCHIVE);
+
+ menu.show(getControl(IDC_IMPORT), handle());
+}
+
+bool Manager::importRepo()
{
if(m_importing) // avoid opening the import dialog twice on windows
return true;
@@ -458,6 +489,81 @@ bool Manager::import()
return ret != 0;
}
+void Manager::importArchive()
+{
+ const auto_char *title = AUTO_STR("Import offline archive");
+
+ const auto_string &path = FileDialog::getOpenFileName(handle(), instance(),
+ title, Path::prefixRoot(Path::DATA), ARCHIVE_FILTER, ARCHIVE_EXT);
+
+ if(path.empty())
+ return;
+
+ try {
+ Archive::import(path, m_reapack);
+ }
+ catch(const reapack_error &e) {
+ const auto_string &desc = make_autostring(e.what());
+
+ auto_char msg[512];
+ auto_snprintf(msg, auto_size(msg),
+ AUTO_STR("An error occured while reading %s.\r\n\r\n%s."),
+ path.c_str(), desc.c_str()
+ );
+
+ MessageBox(handle(), msg, title, MB_OK);
+ }
+}
+
+void Manager::exportArchive()
+{
+ const auto_char *title = AUTO_STR("Export offline archive");
+
+ const auto_string &path = FileDialog::getSaveFileName(handle(), instance(),
+ title, Path::prefixRoot(Path::DATA), ARCHIVE_FILTER, ARCHIVE_EXT);
+
+ if(path.empty())
+ return;
+
+ ThreadPool *pool = new ThreadPool;
+ Dialog *progress = Dialog::Create<Progress>(instance(), parent(), pool);
+
+ try {
+ const size_t count = Archive::create(path, pool, m_reapack);
+
+ const auto finish = [=] {
+ Dialog::Destroy(progress);
+
+ auto_char msg[255];
+ auto_snprintf(msg, auto_size(msg),
+ AUTO_STR("Done! %zu package%s were exported in the archive."),
+ count, count == 1 ? AUTO_STR("") : AUTO_STR("s"));
+ MessageBox(handle(), msg, title, MB_OK);
+
+ delete pool;
+ };
+
+ pool->onDone(finish);
+
+ if(pool->idle())
+ finish();
+ }
+ catch(const reapack_error &e) {
+ Dialog::Destroy(progress);
+ delete pool;
+
+ const auto_string &desc = make_autostring(e.what());
+
+ auto_char msg[512];
+ auto_snprintf(msg, auto_size(msg),
+ AUTO_STR("An error occured while writing into %s.\r\n\r\n%s."),
+ path.c_str(), desc.c_str()
+ );
+ MessageBox(handle(), msg, title, MB_OK);
+ }
+
+}
+
void Manager::launchBrowser()
{
const auto promptApply = [&] {
diff --git a/src/manager.hpp b/src/manager.hpp
@@ -39,7 +39,7 @@ public:
Manager(ReaPack *);
void refresh();
- bool import();
+ bool importRepo();
protected:
void onInit() override;
@@ -71,8 +71,11 @@ private:
void about(int index);
void copyUrl();
void launchBrowser();
+ void importExport();
void options();
void setupNetwork();
+ void importArchive();
+ void exportArchive();
void setChange(int);
bool confirm() const;
diff --git a/src/path.cpp b/src/path.cpp
@@ -234,3 +234,9 @@ UseRootPath::~UseRootPath()
{
Path::s_root = move(m_backup);
}
+
+TempPath::TempPath(const Path &target)
+ : m_target(target), m_temp(target)
+{
+ m_temp[m_temp.size() - 1] += ".part";
+}
diff --git a/src/path.hpp b/src/path.hpp
@@ -79,4 +79,16 @@ private:
Path m_backup;
};
+class TempPath {
+public:
+ TempPath(const Path &target);
+
+ const Path &target() const { return m_target; }
+ const Path &temp() const { return m_temp; }
+
+private:
+ Path m_target;
+ Path m_temp;
+};
+
#endif
diff --git a/src/progress.cpp b/src/progress.cpp
@@ -17,17 +17,17 @@
#include "progress.hpp"
-#include "download.hpp"
+#include "thread.hpp"
#include "resource.hpp"
using namespace std;
-Progress::Progress(DownloadQueue *queue)
+Progress::Progress(ThreadPool *pool)
: Dialog(IDD_PROGRESS_DIALOG),
- m_queue(queue), m_label(nullptr), m_progress(nullptr),
+ m_pool(pool), m_label(nullptr), m_progress(nullptr),
m_done(0), m_total(0)
{
- m_queue->onPush(bind(&Progress::addDownload, this, placeholders::_1));
+ m_pool->onPush(bind(&Progress::addTask, this, placeholders::_1));
}
void Progress::onInit()
@@ -44,7 +44,7 @@ void Progress::onCommand(const int id, int)
{
switch(id) {
case IDCANCEL:
- m_queue->abort();
+ m_pool->abort();
// don't wait until the current downloads are finished
// before getting out of the user way
@@ -59,7 +59,7 @@ void Progress::onTimer(const int id)
stopTimer(id);
}
-void Progress::addDownload(Download *dl)
+void Progress::addTask(ThreadTask *task)
{
m_total++;
updateProgress();
@@ -67,12 +67,12 @@ void Progress::addDownload(Download *dl)
if(!isVisible())
startTimer(100);
- dl->onStart([=] {
- m_currentName = make_autostring(dl->name());
+ task->onStart([=] {
+ m_current = make_autostring(task->summary());
updateProgress();
});
- dl->onFinish([=] {
+ task->onFinish([=] {
m_done++;
updateProgress();
});
@@ -80,9 +80,12 @@ void Progress::addDownload(Download *dl)
void Progress::updateProgress()
{
+ auto_char position[32];
+ auto_snprintf(position, auto_size(position), AUTO_STR("%d of %d"),
+ min(m_done + 1, m_total), m_total);
+
auto_char label[1024];
- auto_snprintf(label, auto_size(label), AUTO_STR("Downloading %d of %d: %s"),
- min(m_done + 1, m_total), m_total, m_currentName.c_str());
+ auto_snprintf(label, auto_size(label), m_current.c_str(), position);
SetWindowText(m_label, label);
@@ -91,7 +94,7 @@ void Progress::updateProgress()
auto_char title[255];
auto_snprintf(title, auto_size(title),
- AUTO_STR("ReaPack: Download in progress (%d%%)"), percent);
+ AUTO_STR("ReaPack: Operation in progress (%d%%)"), percent);
SendMessage(m_progress, PBM_SETPOS, percent, 0);
SetWindowText(handle(), title);
diff --git a/src/progress.hpp b/src/progress.hpp
@@ -22,12 +22,12 @@
#include "encoding.hpp"
-class Download;
-class DownloadQueue;
+class ThreadPool;
+class ThreadTask;
class Progress : public Dialog {
public:
- Progress(DownloadQueue *);
+ Progress(ThreadPool *);
protected:
void onInit() override;
@@ -35,11 +35,11 @@ protected:
void onTimer(int) override;
private:
- void addDownload(Download *);
+ void addTask(ThreadTask *);
void updateProgress();
- DownloadQueue *m_queue;
- auto_string m_currentName;
+ ThreadPool *m_pool;
+ auto_string m_current;
HWND m_label;
HWND m_progress;
diff --git a/src/reapack.cpp b/src/reapack.cpp
@@ -20,6 +20,7 @@
#include "about.hpp"
#include "browser.hpp"
#include "config.hpp"
+#include "download.hpp"
#include "errors.hpp"
#include "filesystem.hpp"
#include "index.hpp"
@@ -80,7 +81,7 @@ ReaPack::ReaPack(REAPER_PLUGIN_HINSTANCE instance)
m_mainWindow = GetMainHwnd();
m_useRootPath = new UseRootPath(resourcePath());
- Download::Init();
+ DownloadContext::GlobalInit();
RichEdit::Init();
FS::mkdir(Path::CACHE);
@@ -105,7 +106,7 @@ ReaPack::~ReaPack()
m_config->write();
delete m_config;
- Download::Cleanup();
+ DownloadContext::GlobalCleanup();
delete m_useRootPath;
}
@@ -199,7 +200,7 @@ void ReaPack::importRemote()
manageRemotes();
- if(!m_manager->import() && autoClose)
+ if(!m_manager->importRepo() && autoClose)
m_manager->close();
}
@@ -287,12 +288,12 @@ void ReaPack::fetchIndexes(const vector<Remote> &remotes,
// (in `parent`) to the progress dialog prevents it from being shown at all
// while still taking the focus away from the manager dialog.
- DownloadQueue *queue = new DownloadQueue;
- Dialog *progress = Dialog::Create<Progress>(m_instance, m_mainWindow, queue);
+ ThreadPool *pool = new ThreadPool;
+ Dialog *progress = Dialog::Create<Progress>(m_instance, m_mainWindow, pool);
auto load = [=] {
Dialog::Destroy(progress);
- delete queue;
+ delete pool;
vector<IndexPtr> indexes;
@@ -309,19 +310,19 @@ void ReaPack::fetchIndexes(const vector<Remote> &remotes,
callback(indexes);
};
- queue->onDone(load);
+ pool->onDone(load);
for(const Remote &remote : remotes)
- doFetchIndex(remote, queue, parent, stale);
+ doFetchIndex(remote, pool, parent, stale);
- if(queue->idle())
+ if(pool->idle())
load();
}
-void ReaPack::doFetchIndex(const Remote &remote, DownloadQueue *queue,
+void ReaPack::doFetchIndex(const Remote &remote, ThreadPool *pool,
HWND parent, const bool stale)
{
- Download *dl = Index::fetch(remote, stale, m_config->network);
+ FileDownload *dl = Index::fetch(remote, stale, m_config->network);
if(!dl)
return;
@@ -342,23 +343,14 @@ void ReaPack::doFetchIndex(const Remote &remote, DownloadQueue *queue,
};
dl->onFinish([=] {
- const Path &path = Index::pathFor(remote.name());
-
- switch(dl->state()) {
- case Download::Success:
- if(!FS::write(path, dl->contents()))
- warn(FS::lastError(), AUTO_STR("Write Failed"));
- break;
- case Download::Failure:
- if(stale || !FS::exists(path))
- warn(dl->contents(), AUTO_STR("Download Failed"));
- break;
- default:
- break;
- }
+ if(!dl->save())
+ warn(FS::lastError(), AUTO_STR("Write Failed"));
+ else if(dl->state() == ThreadTask::Failure &&
+ (stale || !FS::exists(dl->path().target())))
+ warn(dl->error().message, AUTO_STR("Download Failed"));
});
- queue->push(dl);
+ pool->push(dl);
}
IndexPtr ReaPack::loadIndex(const Remote &remote, HWND parent)
@@ -411,8 +403,7 @@ Transaction *ReaPack::setupTransaction()
}
assert(!m_progress);
- m_progress = Dialog::Create<Progress>(m_instance, m_mainWindow,
- m_tx->downloadQueue());
+ m_progress = Dialog::Create<Progress>(m_instance, m_mainWindow, m_tx->threadPool());
m_tx->onFinish([=] {
Dialog::Destroy(m_progress);
diff --git a/src/reapack.hpp b/src/reapack.hpp
@@ -31,11 +31,11 @@
class About;
class Browser;
class Config;
-class DownloadQueue;
class Index;
class Manager;
class Progress;
class Remote;
+class ThreadPool;
class Transaction;
typedef std::shared_ptr<const Index> IndexPtr;
@@ -89,7 +89,7 @@ public:
private:
void registerSelf();
- void doFetchIndex(const Remote &remote, DownloadQueue *, HWND, bool stale);
+ void doFetchIndex(const Remote &remote, ThreadPool *, HWND, bool stale);
IndexPtr loadIndex(const Remote &remote, HWND);
void teardownTransaction();
diff --git a/src/receipt.hpp b/src/receipt.hpp
@@ -23,6 +23,7 @@
#include <unordered_set>
#include <vector>
+#include "errors.hpp"
#include "registry.hpp"
class Index;
@@ -40,11 +41,6 @@ struct InstallTicket {
class Receipt {
public:
- struct Error {
- std::string message;
- std::string title;
- };
-
Receipt();
bool empty() const;
@@ -60,8 +56,8 @@ public:
void addRemovals(const std::set<Path> &);
const std::set<Path> &removals() const { return m_removals; }
- void addError(const Error &err) { m_errors.push_back(err); }
- const std::vector<Error> &errors() const { return m_errors; }
+ void addError(const ErrorInfo &err) { m_errors.push_back(err); }
+ const std::vector<ErrorInfo> &errors() const { return m_errors; }
bool hasErrors() const { return !m_errors.empty(); }
private:
@@ -71,7 +67,7 @@ private:
std::vector<InstallTicket> m_installs;
std::vector<InstallTicket> m_updates;
std::set<Path> m_removals;
- std::vector<Error> m_errors;
+ std::vector<ErrorInfo> m_errors;
std::unordered_set<IndexPtr> m_indexes; // keep them alive!
};
diff --git a/src/report.cpp b/src/report.cpp
@@ -133,11 +133,11 @@ void Report::printErrors()
const auto start = m_stream.tellp();
- for(const Receipt::Error &err : m_receipt.errors()) {
+ for(const ErrorInfo &err : m_receipt.errors()) {
if(m_stream.tellp() != start)
m_stream << "\r\n";
- m_stream << err.title << ":\r\n";
+ m_stream << err.context << ":\r\n";
m_stream.indented(err.message);
}
}
diff --git a/src/resource.rc b/src/resource.rc
@@ -35,8 +35,8 @@ BEGIN
CONTROL "", IDC_LIST, WC_LISTVIEW, LVS_REPORT | LVS_SHOWSELALWAYS |
WS_BORDER | WS_TABSTOP, 5, 18, 360, 170
PUSHBUTTON "&Browse packages", IDC_BROWSE, 5, 191, 75, 14
- PUSHBUTTON "&Import...", IDC_IMPORT, 83, 191, 45, 14
- PUSHBUTTON "&Options...", IDC_OPTIONS, 131, 191, 45, 14
+ PUSHBUTTON "&Import/export...", IDC_IMPORT, 83, 191, 65, 14
+ PUSHBUTTON "&Options...", IDC_OPTIONS, 151, 191, 45, 14
DEFPUSHBUTTON "&OK", IDOK, 238, 191, 40, 14
PUSHBUTTON "&Cancel", IDCANCEL, 281, 191, 40, 14
PUSHBUTTON "&Apply", IDAPPLY, 324, 191, 40, 14
diff --git a/src/source.cpp b/src/source.cpp
@@ -83,16 +83,6 @@ void Source::setSections(int sections)
m_sections = sections;
}
-string Source::fullName() const
-{
- if(!m_version)
- return Path(file()).basename();
- else if(m_file.empty())
- return m_version->fullName();
-
- return m_version->fullName() + " (" + Path(m_file).basename() + ")";
-}
-
Path Source::targetPath() const
{
Path path;
diff --git a/src/source.hpp b/src/source.hpp
@@ -39,10 +39,10 @@ public:
static Section detectSection(const std::string &category);
Source(const std::string &file, const std::string &url, const Version *);
+ const Version *version() const { return m_version; }
void setPlatform(Platform p) { m_platform = p; }
Platform platform() const { return m_platform; }
-
void setTypeOverride(Package::Type t) { m_type = t; }
Package::Type typeOverride() const { return m_type; }
Package::Type type() const;
@@ -51,9 +51,6 @@ public:
void setSections(int);
int sections() const { return m_sections; }
- const Version *version() const { return m_version; }
-
- std::string fullName() const;
Path targetPath() const;
private:
diff --git a/src/task.cpp b/src/task.cpp
@@ -17,7 +17,9 @@
#include "task.hpp"
+#include "archive.hpp"
#include "config.hpp"
+#include "download.hpp"
#include "errors.hpp"
#include "filesystem.hpp"
#include "index.hpp"
@@ -30,9 +32,9 @@ Task::Task(Transaction *tx) : m_tx(tx)
}
InstallTask::InstallTask(const Version *ver, const bool pin,
- const Registry::Entry &re, Transaction *tx)
- : Task(tx), m_version(ver), m_pin(pin), m_oldEntry(move(re)), m_fail(false),
- m_index(ver->package()->category()->index()->shared_from_this())
+ const Registry::Entry &re, const ArchiveReaderPtr &reader, Transaction *tx)
+ : Task(tx), m_version(ver), m_pin(pin), m_oldEntry(move(re)), m_reader(reader),
+ m_fail(false), m_index(ver->package()->category()->index()->shared_from_this())
{
}
@@ -61,35 +63,40 @@ bool InstallTask::start()
}
for(const Source *src : m_version->sources()) {
- const NetworkOpts &opts = tx()->config()->network;
- Download *dl = new Download(src->fullName(), src->url(), opts);
- dl->onFinish(bind(&InstallTask::saveSource, this, dl, src));
+ const Path &targetPath = src->targetPath();
- tx()->downloadQueue()->push(dl);
+ const auto old = find_if(m_oldFiles.begin(), m_oldFiles.end(),
+ [&](const Registry::File &f) { return f.path == targetPath; });
+
+ if(old != m_oldFiles.end())
+ m_oldFiles.erase(old);
+
+ if(m_reader) {
+ FileExtractor *ex = new FileExtractor(targetPath, m_reader);
+ push(ex, ex->path());
+ }
+ else {
+ const NetworkOpts &opts = tx()->config()->network;
+ FileDownload *dl = new FileDownload(targetPath, src->url(), opts);
+ push(dl, dl->path());
+ }
}
return true;
}
-void InstallTask::saveSource(Download *dl, const Source *src)
+void InstallTask::push(ThreadTask *job, const TempPath &path)
{
- const Path &targetPath = src->targetPath();
-
- Path tmpPath(targetPath);
- tmpPath[tmpPath.size() - 1] += ".new";
-
- m_newFiles.push_back({targetPath, tmpPath});
+ job->onStart([=] { m_newFiles.push_back(path); });
+ job->onFinish([=] {
+ m_waiting.erase(job);
- const auto old = find_if(m_oldFiles.begin(), m_oldFiles.end(),
- [&](const Registry::File &f) { return f.path == targetPath; });
-
- if(old != m_oldFiles.end())
- m_oldFiles.erase(old);
+ if(job->state() != ThreadTask::Success)
+ rollback();
+ });
- if(!tx()->saveFile(dl, tmpPath)) {
- rollback();
- return;
- }
+ m_waiting.insert(job);
+ tx()->threadPool()->push(job);
}
void InstallTask::commit()
@@ -97,15 +104,10 @@ void InstallTask::commit()
if(m_fail)
return;
- for(const PathGroup &paths : m_newFiles) {
-#ifdef _WIN32
- // TODO: rename to .old
- FS::remove(paths.target);
-#endif
-
- if(!FS::rename(paths.temp, paths.target)) {
+ for(const TempPath &paths : m_newFiles) {
+ if(!FS::rename(paths)) {
tx()->receipt()->addError({"Cannot rename to target: " + FS::lastError(),
- paths.target.join()});
+ paths.target().join()});
// it's a bit late to rollback here as some files might already have been
// overwritten. at least we can delete the temporary files
@@ -143,8 +145,11 @@ void InstallTask::commit()
void InstallTask::rollback()
{
- for(const PathGroup &paths : m_newFiles)
- FS::removeRecursive(paths.temp);
+ for(const TempPath &paths : m_newFiles)
+ FS::removeRecursive(paths.temp());
+
+ for(ThreadTask *job : m_waiting)
+ job->abort();
m_fail = true;
}
diff --git a/src/task.hpp b/src/task.hpp
@@ -22,14 +22,17 @@
#include "registry.hpp"
#include <set>
+#include <unordered_set>
#include <vector>
-class Download;
+class ArchiveReader;
class Index;
class Source;
+class ThreadTask;
class Transaction;
class Version;
+typedef std::shared_ptr<ArchiveReader> ArchiveReaderPtr;
typedef std::shared_ptr<const Index> IndexPtr;
class Task {
@@ -53,24 +56,26 @@ private:
class InstallTask : public Task {
public:
- InstallTask(const Version *ver, bool pin, const Registry::Entry &, Transaction *);
+ InstallTask(const Version *ver, bool pin, const Registry::Entry &,
+ const ArchiveReaderPtr &, Transaction *);
bool start() override;
void commit() override;
void rollback() override;
private:
- struct PathGroup { Path target; Path temp; };
-
- void saveSource(Download *, const Source *);
+ void push(ThreadTask *, const TempPath &);
const Version *m_version;
bool m_pin;
Registry::Entry m_oldEntry;
+ ArchiveReaderPtr m_reader;
+
bool m_fail;
IndexPtr m_index; // keep in memory
std::vector<Registry::File> m_oldFiles;
- std::vector<PathGroup> m_newFiles;
+ std::vector<TempPath> m_newFiles;
+ std::unordered_set<ThreadTask *> m_waiting;
};
class UninstallTask : public Task {
diff --git a/src/thread.cpp b/src/thread.cpp
@@ -0,0 +1,208 @@
+/* ReaPack: Package manager for REAPER
+ * Copyright (C) 2015-2017 Christian Fillion
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "thread.hpp"
+
+#include "download.hpp"
+
+#include <reaper_plugin_functions.h>
+
+using namespace std;
+
+ThreadNotifier *ThreadNotifier::s_instance = nullptr;
+
+ThreadTask::ThreadTask()
+ : m_state(Idle), m_abort(false)
+{
+ ThreadNotifier::get()->start();
+}
+
+ThreadTask::~ThreadTask()
+{
+ ThreadNotifier::get()->stop();
+}
+
+void ThreadTask::setState(const State state)
+{
+ m_state = state;
+
+ switch(state) {
+ case Idle:
+ break;
+ case Running:
+ m_onStart();
+ break;
+ case Success:
+ case Failure:
+ case Aborted:
+ m_onFinish();
+ m_cleanupHandler();
+ break;
+ }
+}
+
+void ThreadTask::finish(const State state, const ErrorInfo &error)
+{
+ m_error = error;
+
+ ThreadNotifier::get()->notify({this, state});
+};
+
+WorkerThread::WorkerThread() : m_exit(false)
+{
+ m_wake = CreateEvent(nullptr, true, false, AUTO_STR("WakeEvent"));
+ m_thread = CreateThread(nullptr, 0, run, (void *)this, 0, nullptr);
+}
+
+WorkerThread::~WorkerThread()
+{
+ m_exit = true;
+ SetEvent(m_wake);
+
+ WaitForSingleObject(m_thread, INFINITE);
+
+ CloseHandle(m_wake);
+ CloseHandle(m_thread);
+}
+
+DWORD WINAPI WorkerThread::run(void *ptr)
+{
+ WorkerThread *thread = static_cast<WorkerThread *>(ptr);
+
+ DownloadContext context;
+
+ while(!thread->m_exit) {
+ while(ThreadTask *task = thread->nextTask())
+ task->run(&context);
+
+ ResetEvent(thread->m_wake);
+ WaitForSingleObject(thread->m_wake, INFINITE);
+ }
+
+ return 0;
+}
+
+ThreadTask *WorkerThread::nextTask()
+{
+ WDL_MutexLock lock(&m_mutex);
+
+ if(m_queue.empty())
+ return nullptr;
+
+ ThreadTask *task = m_queue.front();
+ m_queue.pop();
+ return task;
+}
+
+void WorkerThread::push(ThreadTask *task)
+{
+ WDL_MutexLock lock(&m_mutex);
+
+ m_queue.push(task);
+ SetEvent(m_wake);
+}
+
+ThreadPool::~ThreadPool()
+{
+ // don't emit ThreadPool::onAbort from the destructor
+ // which is most likely to cause a crash
+ m_onAbort.disconnect_all_slots();
+
+ abort();
+}
+
+void ThreadPool::push(ThreadTask *task)
+{
+ m_onPush(task);
+ m_running.insert(task);
+
+ task->onFinish([=] {
+ m_running.erase(task);
+
+ // call m_onDone() only after every onFinish slots ran
+ if(m_running.empty())
+ m_onDone();
+ });
+
+ task->setCleanupHandler([=] { delete task; });
+
+ const size_t nextThread = m_running.size() % m_pool.size();
+ auto &thread = task->concurrent() ? m_pool[nextThread] : m_pool.front();
+ if(!thread)
+ thread = make_unique<WorkerThread>();
+
+ thread->push(task);
+}
+
+void ThreadPool::abort()
+{
+ for(ThreadTask *task : m_running)
+ task->abort();
+
+ m_onAbort();
+}
+
+ThreadNotifier *ThreadNotifier::get()
+{
+ if(!s_instance)
+ s_instance = new ThreadNotifier;
+
+ return s_instance;
+}
+
+void ThreadNotifier::start()
+{
+ if(!m_active++)
+ plugin_register("timer", (void *)tick);
+}
+
+void ThreadNotifier::stop()
+{
+ --m_active;
+}
+
+void ThreadNotifier::notify(const Notification ¬if)
+{
+ WDL_MutexLock lock(&m_mutex);
+
+ m_queue.push(notif);
+}
+
+void ThreadNotifier::tick()
+{
+ ThreadNotifier *instance = ThreadNotifier::get();
+ instance->processQueue();
+
+ // doing this in stop() would cause a use after free of m_mutex in processQueue
+ if(!instance->m_active) {
+ plugin_register("-timer", (void *)tick);
+
+ delete s_instance;
+ s_instance = nullptr;
+ }
+}
+
+void ThreadNotifier::processQueue()
+{
+ WDL_MutexLock lock(&m_mutex);
+
+ while(!m_queue.empty()) {
+ const Notification ¬if = m_queue.front();
+ notif.first->setState(notif.second);
+ m_queue.pop();
+ }
+}
diff --git a/src/thread.hpp b/src/thread.hpp
@@ -0,0 +1,158 @@
+/* ReaPack: Package manager for REAPER
+ * Copyright (C) 2015-2017 Christian Fillion
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef REAPACK_THREAD_HPP
+#define REAPACK_THREAD_HPP
+
+#include "errors.hpp"
+
+#include <array>
+#include <atomic>
+#include <functional>
+#include <queue>
+#include <unordered_set>
+
+#include <boost/signals2.hpp>
+#include <WDL/mutex.h>
+
+#ifdef _WIN32
+#include <windows.h>
+#else
+#include <swell-types.h>
+#endif
+
+struct DownloadContext;
+
+class ThreadTask {
+public:
+ enum State {
+ Idle,
+ Running,
+ Success,
+ Failure,
+ Aborted,
+ };
+
+ typedef boost::signals2::signal<void ()> VoidSignal;
+ typedef std::function<void ()> CleanupHandler;
+
+ ThreadTask();
+ virtual ~ThreadTask();
+
+ virtual bool concurrent() const = 0;
+ virtual void run(DownloadContext *) = 0;
+
+ const std::string &summary() const { return m_summary; }
+ void setState(State);
+ State state() const { return m_state; }
+ const ErrorInfo &error() { return m_error; }
+
+ void onStart(const VoidSignal::slot_type &slot) { m_onStart.connect(slot); }
+ void onFinish(const VoidSignal::slot_type &slot) { m_onFinish.connect(slot); }
+ void setCleanupHandler(const CleanupHandler &cb) { m_cleanupHandler = cb; }
+
+ bool aborted() const { return m_abort; }
+ void abort() { m_abort = true; }
+
+protected:
+ void setSummary(const std::string &s) { m_summary = s; }
+ void finish(State, const ErrorInfo & = {});
+
+private:
+ std::string m_summary;
+ State m_state;
+ ErrorInfo m_error;
+ std::atomic_bool m_abort;
+
+ VoidSignal m_onStart;
+ VoidSignal m_onFinish;
+ CleanupHandler m_cleanupHandler;
+};
+
+class WorkerThread {
+public:
+ WorkerThread();
+ ~WorkerThread();
+
+ void push(ThreadTask *);
+ void clear();
+
+private:
+ static DWORD WINAPI run(void *);
+ ThreadTask *nextTask();
+
+ HANDLE m_wake;
+ HANDLE m_thread;
+ std::atomic_bool m_exit;
+ WDL_Mutex m_mutex;
+ std::queue<ThreadTask *> m_queue;
+};
+
+class ThreadPool {
+public:
+ typedef boost::signals2::signal<void ()> VoidSignal;
+ typedef boost::signals2::signal<void (ThreadTask *)> TaskSignal;
+
+ ThreadPool() {}
+ ThreadPool(const ThreadPool &) = delete;
+ ~ThreadPool();
+
+ void push(ThreadTask *);
+ void abort();
+
+ bool idle() const { return m_running.empty(); }
+
+ void onPush(const TaskSignal::slot_type &slot) { m_onPush.connect(slot); }
+ void onAbort(const VoidSignal::slot_type &slot) { m_onAbort.connect(slot); }
+ void onDone(const VoidSignal::slot_type &slot) { m_onDone.connect(slot); }
+
+private:
+ std::array<std::unique_ptr<WorkerThread>, 3> m_pool;
+ std::unordered_set<ThreadTask *> m_running;
+
+ TaskSignal m_onPush;
+ VoidSignal m_onAbort;
+ VoidSignal m_onDone;
+};
+
+// This singleton class receives state change notifications from a
+// worker thread and applies them in the main thread
+class ThreadNotifier {
+ typedef std::pair<ThreadTask *, ThreadTask::State> Notification;
+
+public:
+ static ThreadNotifier *get();
+
+ void start();
+ void stop();
+
+ void notify(const Notification &);
+
+private:
+ static ThreadNotifier *s_instance;
+ static void tick();
+
+ ThreadNotifier() : m_active(0) {}
+ ~ThreadNotifier() = default;
+ void processQueue();
+
+ WDL_Mutex m_mutex;
+ size_t m_active;
+ std::queue<Notification> m_queue;
+};
+
+#endif
diff --git a/src/transaction.cpp b/src/transaction.cpp
@@ -17,8 +17,9 @@
#include "transaction.hpp"
+#include "archive.hpp"
#include "config.hpp"
-#include "encoding.hpp"
+#include "download.hpp"
#include "errors.hpp"
#include "filesystem.hpp"
#include "index.hpp"
@@ -36,13 +37,20 @@ Transaction::Transaction(Config *config)
// don't keep pre-install pushes (for conflict checks); released in runTasks
m_registry.savepoint();
- m_downloadQueue.onAbort([=] {
+ m_threadPool.onPush([this] (ThreadTask *task) {
+ task->onFinish([=] {
+ if(task->state() == ThreadTask::Failure)
+ m_receipt.addError(task->error());
+ });
+ });
+
+ m_threadPool.onAbort([this] {
m_isCancelled = true;
queue<HostTicket>().swap(m_regQueue);
});
// run tasks after fetching indexes
- m_downloadQueue.onDone(bind(&Transaction::runTasks, this));
+ m_threadPool.onDone(bind(&Transaction::runTasks, this));
}
void Transaction::synchronize(const Remote &remote,
@@ -105,12 +113,12 @@ void Transaction::synchronize(const Package *pkg, const InstallOpts &opts)
else if(regEntry.pinned || latest->name() < regEntry.version)
return;
- m_nextQueue.push(make_shared<InstallTask>(latest, false, regEntry, this));
+ m_nextQueue.push(make_shared<InstallTask>(latest, false, regEntry, nullptr, this));
}
void Transaction::fetchIndex(const Remote &remote, const function<void()> &cb)
{
- Download *dl = Index::fetch(remote, true, m_config->network);
+ FileDownload *dl = Index::fetch(remote, true, m_config->network);
if(!dl) {
// the index was last downloaded less than a few seconds ago
@@ -119,17 +127,20 @@ void Transaction::fetchIndex(const Remote &remote, const function<void()> &cb)
}
dl->onFinish([=] {
- if(saveFile(dl, Index::pathFor(dl->name())))
+ if(!dl->save())
+ m_receipt.addError({FS::lastError(), dl->path().target().join()});
+ else if(dl->state() == ThreadTask::Success)
cb();
});
- m_downloadQueue.push(dl);
+ m_threadPool.push(dl);
}
-void Transaction::install(const Version *ver, const bool pin)
+void Transaction::install(const Version *ver,
+ const bool pin, const ArchiveReaderPtr &reader)
{
const auto &oldEntry = m_registry.getEntry(ver->package());
- m_nextQueue.push(make_shared<InstallTask>(ver, pin, oldEntry, this));
+ m_nextQueue.push(make_shared<InstallTask>(ver, pin, oldEntry, reader, this));
}
void Transaction::registerAll(const Remote &remote)
@@ -168,21 +179,6 @@ void Transaction::uninstall(const Registry::Entry &entry)
m_nextQueue.push(make_shared<UninstallTask>(entry, this));
}
-bool Transaction::saveFile(Download *dl, const Path &path)
-{
- if(dl->state() != Download::Success) {
- m_receipt.addError({dl->contents(), dl->url()});
- return false;
- }
-
- if(!FS::write(path, dl->contents())) {
- m_receipt.addError({FS::lastError(), path.join()});
- return false;
- }
-
- return true;
-}
-
bool Transaction::runTasks()
{
if(!m_nextQueue.empty()) {
@@ -244,7 +240,7 @@ bool Transaction::runTasks()
bool Transaction::commitTasks()
{
// wait until all running tasks are ready
- if(!m_downloadQueue.idle())
+ if(!m_threadPool.idle())
return false;
// finish current tasks
diff --git a/src/transaction.hpp b/src/transaction.hpp
@@ -18,10 +18,10 @@
#ifndef REAPACK_TRANSACTION_HPP
#define REAPACK_TRANSACTION_HPP
-#include "download.hpp"
#include "receipt.hpp"
#include "registry.hpp"
#include "task.hpp"
+#include "thread.hpp"
#include <boost/optional.hpp>
#include <boost/signals2.hpp>
@@ -30,6 +30,7 @@
#include <set>
#include <unordered_set>
+class ArchiveReader;
class Config;
class Path;
class Remote;
@@ -53,7 +54,7 @@ public:
void synchronize(const Remote &,
boost::optional<bool> forceAutoInstall = boost::none);
- void install(const Version *, bool pin = false);
+ void install(const Version *, bool pin = false, const ArchiveReaderPtr & = nullptr);
void setPinned(const Registry::Entry &, bool pinned);
void uninstall(const Remote &);
void uninstall(const Registry::Entry &);
@@ -65,11 +66,10 @@ public:
Receipt *receipt() { return &m_receipt; }
Registry *registry() { return &m_registry; }
const Config *config() { return m_config; }
- DownloadQueue *downloadQueue() { return &m_downloadQueue; }
+ ThreadPool *threadPool() { return &m_threadPool; }
void registerAll(bool add, const Registry::Entry &);
void registerFile(const HostTicket &t) { m_regQueue.push(t); }
- bool saveFile(Download *, const Path &);
private:
class CompareTask {
@@ -101,7 +101,7 @@ private:
std::unordered_set<std::string> m_inhibited;
std::unordered_set<Registry::Entry> m_obsolete;
- DownloadQueue m_downloadQueue;
+ ThreadPool m_threadPool;
TaskQueue m_nextQueue;
std::queue<TaskQueue> m_taskQueues;
std::queue<TaskPtr> m_runningTasks;
diff --git a/test/path.cpp b/test/path.cpp
@@ -236,3 +236,10 @@ TEST_CASE("append full paths") {
REQUIRE(a == Path("a/b/c/d/e/f"));
}
+
+TEST_CASE("temporary path") {
+ TempPath a(Path("hello/world"));
+
+ REQUIRE(a.target() == Path("hello/world"));
+ REQUIRE(a.temp() == Path("hello/world.part"));
+}
diff --git a/test/source.cpp b/test/source.cpp
@@ -127,22 +127,6 @@ TEST_CASE("empty source url", M) {
}
}
-TEST_CASE("source full name", M) {
- MAKE_VERSION;
-
- SECTION("with file name") {
- const Source source("path/to/file", "b", &ver);
-
- REQUIRE(source.fullName() == ver.fullName() + " (file)");
- }
-
- SECTION("without file name") {
- const Source source({}, "b", &ver);
-
- REQUIRE(source.fullName() == ver.fullName());
- }
-}
-
TEST_CASE("source target path", M) {
MAKE_VERSION;
diff --git a/win32.tup b/win32.tup
@@ -25,7 +25,7 @@ SQLFLAGS += /DSQLITE_OMIT_COMPILEOPTION_DIAGS /DSQLITE_OMIT_CAST
SQLFLAGS += /DSQLITE_OMIT_CHECK /DSQLITE_OMIT_DECLTYPE /DSQLITE_OMIT_DEPRECATED
LD := $(WRAP) link
-LDFLAGS := /nologo User32.lib Shell32.lib Gdi32.lib
+LDFLAGS := /nologo User32.lib Shell32.lib Gdi32.lib Comdlg32.lib
LDFLAGS += vendor/libcurl@(SUFFIX)/lib/libcurl_a.lib
LDFLAGS += $(TUP_VARIANTDIR)/src/resource.res
LDFLAGS += $(TUP_VARIANTDIR)/build/vendor/sqlite3.o