reapack

Package manager for REAPER
Log | Files | Refs | Submodules | README | LICENSE

commit ac0ec38375acae2a0fcde1d65786bb7d4461c8bc
parent 7e1628eac5bcd700bdd5bc8cf679bf472b8ca4ee
Author: cfillion <cfillion@users.noreply.github.com>
Date:   Mon, 27 Feb 2017 16:59:21 -0500

Merge branch 'archiving'

Diffstat:
MTupfile | 11++++++++---
Mmacosx.tup | 13+++++++------
Msrc/about.cpp | 1+
Asrc/archive.cpp | 377+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/archive.hpp | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/download.cpp | 279++++++++++++++++++++++---------------------------------------------------------
Msrc/download.hpp | 149+++++++++++++++++++++----------------------------------------------------------
Msrc/errors.hpp | 5+++++
Asrc/filedialog.cpp | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/filedialog.hpp | 38++++++++++++++++++++++++++++++++++++++
Msrc/filesystem.cpp | 27++++++++++++++++++++++++---
Msrc/filesystem.hpp | 4++++
Msrc/import.cpp | 13+++++++------
Msrc/import.hpp | 4++--
Msrc/index.cpp | 7+++++--
Msrc/index.hpp | 4++--
Msrc/manager.cpp | 120++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Msrc/manager.hpp | 5++++-
Msrc/path.cpp | 6++++++
Msrc/path.hpp | 12++++++++++++
Msrc/progress.cpp | 27+++++++++++++++------------
Msrc/progress.hpp | 12++++++------
Msrc/reapack.cpp | 47+++++++++++++++++++----------------------------
Msrc/reapack.hpp | 4++--
Msrc/receipt.hpp | 12++++--------
Msrc/report.cpp | 4++--
Msrc/resource.rc | 4++--
Msrc/source.cpp | 10----------
Msrc/source.hpp | 5+----
Msrc/task.cpp | 71++++++++++++++++++++++++++++++++++++++---------------------------------
Msrc/task.hpp | 17+++++++++++------
Asrc/thread.cpp | 208+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/thread.hpp | 158+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/transaction.cpp | 46+++++++++++++++++++++-------------------------
Msrc/transaction.hpp | 10+++++-----
Mtest/path.cpp | 7+++++++
Mtest/source.cpp | 16----------------
Mwin32.tup | 2+-
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 &notif) -{ - 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 &notif = 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 &notif) +{ + 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 &notif = 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