diff --git a/.travis.yml b/.travis.yml index 99df7d698..bcf1e5b9d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,28 @@ language: cpp matrix: include: + # Ubuntu Bionic (18.04) with GCC 7 + - os: linux + dist: bionic + addons: + apt: + sources: + - sourceline: 'ppa:deadsnakes/ppa' # for Python 3.7 (required by Meson) + packages: + - libgtest-dev + - libboost-dev + - python3.6 + - python3-urllib3 + - ninja-build + before_install: + - wget https://bootstrap.pypa.io/get-pip.py + - /usr/bin/python3.6 get-pip.py --user + install: + - /usr/bin/python3.6 $HOME/.local/bin/pip install --user meson + env: + - MATRIX_EVAL="export PATH=\$HOME/.local/bin:\$PATH" + + # Ubuntu Trusty (16.04) with GCC 6 - os: linux dist: trusty addons: @@ -25,8 +47,9 @@ matrix: - /usr/bin/python3.6 $HOME/.local/bin/pip install --user meson env: # use gold as workaround for https://sourceware.org/bugzilla/show_bug.cgi?id=17068 - - MATRIX_EVAL="export CC=gcc-6 CXX=g++-6 LDFLAGS=-fuse-ld=gold PATH=$HOME/.local/bin:$PATH" + - MATRIX_EVAL="export CC='ccache gcc-6' CXX='ccache g++-6' LDFLAGS=-fuse-ld=gold PATH=\$HOME/.local/bin:\$PATH" + # Ubuntu Trusty (16.04) with GCC 8 - os: linux dist: trusty addons: @@ -50,25 +73,37 @@ matrix: - /usr/bin/python3.6 $HOME/.local/bin/pip install --user meson env: # use gold as workaround for https://sourceware.org/bugzilla/show_bug.cgi?id=17068 - - MATRIX_EVAL="export CC=gcc-8 CXX=g++-8 LDFLAGS=-fuse-ld=gold PATH=$HOME/.local/bin:$PATH" + - MATRIX_EVAL="export CC='ccache gcc-8' CXX='ccache g++-8' LDFLAGS=-fuse-ld=gold PATH=\$HOME/.local/bin:\$PATH" - os: osx - osx_image: xcode9.3beta + osx_image: xcode9.4 + addons: + homebrew: + packages: + - ccache + - meson env: - - MATRIX_EVAL="" + - MATRIX_EVAL="export PATH=/usr/local/opt/ccache/libexec:$PATH HOMEBREW_NO_ANALYTICS=1" cache: - - apt - - ccache + apt: true + ccache: true + directories: + - $HOME/Library/Caches/Homebrew + +before_cache: + - test "$TRAVIS_OS_NAME" != "osx" || brew cleanup before_install: - eval "${MATRIX_EVAL}" - # C++14 - - test "$TRAVIS_OS_NAME" != "osx" || brew update install: # C++14 - - test "$TRAVIS_OS_NAME" != "osx" || brew install ccache meson + + # Work around "Target /usr/local/lib/libgtest.a is a symlink + # belonging to nss. You can unlink it" during gtest install + - test "$TRAVIS_OS_NAME" != "osx" || brew unlink nss + - test "$TRAVIS_OS_NAME" != "osx" || brew install --HEAD https://gist.githubusercontent.com/Kronuz/96ac10fbd8472eb1e7566d740c4034f8/raw/gtest.rb before_script: diff --git a/NEWS b/NEWS index 9c8ec529f..0b7be51e1 100644 --- a/NEWS +++ b/NEWS @@ -30,6 +30,16 @@ ver 0.22 (not yet released) * switch to C++17 - GCC 7 or clang 4 (or newer) recommended +ver 0.21.18 (2019/12/24) +* protocol + - work around Mac OS X bug in the ISO 8601 parser +* output + - alsa: fix hang bug with ALSA "null" outputs +* storage + - curl: fix crash bug +* drop support for CURL versions older than 7.32.0 +* reduce unnecessary CPU wakeups + ver 0.21.17 (2019/12/16) * protocol - relax the ISO 8601 parser: allow omitting field separators, the diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 80fa55394..5b9f57b60 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -2,8 +2,8 @@ + android:versionCode="41" + android:versionName="0.21.18"> diff --git a/src/event/Loop.cxx b/src/event/Loop.cxx index 75e2e0ae2..164b2905d 100644 --- a/src/event/Loop.cxx +++ b/src/event/Loop.cxx @@ -137,7 +137,8 @@ static constexpr int ExportTimeoutMS(std::chrono::steady_clock::duration timeout) { return timeout >= timeout.zero() - ? int(std::chrono::duration_cast(timeout).count()) + /* round up (+1) to avoid unnecessary wakeups */ + ? int(std::chrono::duration_cast(timeout).count()) + 1 : -1; } @@ -220,7 +221,6 @@ EventLoop::Run() noexcept } while (!quit); #ifndef NDEBUG - assert(busy); assert(thread.IsInside()); #endif } diff --git a/src/event/MultiSocketMonitor.cxx b/src/event/MultiSocketMonitor.cxx index 3076ab4d6..4a9d0d33d 100644 --- a/src/event/MultiSocketMonitor.cxx +++ b/src/event/MultiSocketMonitor.cxx @@ -22,6 +22,10 @@ #include +#ifdef USE_EPOLL +#include +#endif + #ifndef _WIN32 #include #endif @@ -37,17 +41,42 @@ MultiSocketMonitor::Reset() noexcept assert(GetEventLoop().IsInside()); fds.clear(); +#ifdef USE_EPOLL + always_ready_fds.clear(); +#endif IdleMonitor::Cancel(); timeout_event.Cancel(); ready = refresh = false; } +bool +MultiSocketMonitor::AddSocket(SocketDescriptor fd, unsigned events) noexcept +{ + fds.emplace_front(*this, fd); + bool success = fds.front().Schedule(events); + if (!success) { + fds.pop_front(); + +#ifdef USE_EPOLL + if (errno == EPERM) + /* not supported by epoll (e.g. "/dev/null"): + add it to the "always ready" list */ + always_ready_fds.push_front({fd, events}); +#endif + } + + return success; +} + void MultiSocketMonitor::ClearSocketList() noexcept { assert(GetEventLoop().IsInside()); fds.clear(); +#ifdef USE_EPOLL + always_ready_fds.clear(); +#endif } #ifndef _WIN32 @@ -55,6 +84,10 @@ MultiSocketMonitor::ClearSocketList() noexcept void MultiSocketMonitor::ReplaceSocketList(pollfd *pfds, unsigned n) noexcept { +#ifdef USE_EPOLL + always_ready_fds.clear(); +#endif + pollfd *const end = pfds + n; UpdateSocketList([pfds, end](SocketDescriptor fd) -> unsigned { @@ -64,9 +97,7 @@ MultiSocketMonitor::ReplaceSocketList(pollfd *pfds, unsigned n) noexcept if (i == end) return 0; - auto events = i->events; - i->events = 0; - return events; + return std::exchange(i->events, 0); }); for (auto i = pfds; i != end; ++i) @@ -79,7 +110,20 @@ MultiSocketMonitor::ReplaceSocketList(pollfd *pfds, unsigned n) noexcept void MultiSocketMonitor::Prepare() noexcept { - const auto timeout = PrepareSockets(); + auto timeout = PrepareSockets(); + +#ifdef USE_EPOLL + if (!always_ready_fds.empty()) { + /* if there was at least one file descriptor not + supported by epoll, install a very short timeout + because we assume it's always ready */ + constexpr std::chrono::steady_clock::duration ready_timeout = + std::chrono::milliseconds(1); + if (timeout < timeout.zero() || timeout > ready_timeout) + timeout = ready_timeout; + } +#endif + if (timeout >= timeout.zero()) timeout_event.Schedule(timeout); else diff --git a/src/event/MultiSocketMonitor.hxx b/src/event/MultiSocketMonitor.hxx index d8f4ac635..2db5d1db8 100644 --- a/src/event/MultiSocketMonitor.hxx +++ b/src/event/MultiSocketMonitor.hxx @@ -49,12 +49,10 @@ class MultiSocketMonitor : IdleMonitor unsigned revents; public: - SingleFD(MultiSocketMonitor &_multi, SocketDescriptor _fd, - unsigned events) noexcept + SingleFD(MultiSocketMonitor &_multi, + SocketDescriptor _fd) noexcept :SocketMonitor(_fd, _multi.GetEventLoop()), - multi(_multi), revents(0) { - Schedule(events); - } + multi(_multi), revents(0) {} SocketDescriptor GetSocket() const noexcept { return SocketMonitor::GetSocket(); @@ -85,8 +83,6 @@ class MultiSocketMonitor : IdleMonitor } }; - friend class SingleFD; - TimerEvent timeout_event; /** @@ -105,6 +101,21 @@ class MultiSocketMonitor : IdleMonitor std::forward_list fds; +#ifdef USE_EPOLL + struct AlwaysReady { + const SocketDescriptor fd; + const unsigned revents; + }; + + /** + * A list of file descriptors which are always ready. This is + * a kludge needed because the ALSA output plugin gives us a + * file descriptor to /dev/null, which is incompatible with + * epoll (epoll_ctl() returns -EPERM). + */ + std::forward_list always_ready_fds; +#endif + public: static constexpr unsigned READ = SocketMonitor::READ; static constexpr unsigned WRITE = SocketMonitor::WRITE; @@ -146,9 +157,7 @@ public: * * May only be called from PrepareSockets(). */ - void AddSocket(SocketDescriptor fd, unsigned events) noexcept { - fds.emplace_front(*this, fd, events); - } + bool AddSocket(SocketDescriptor fd, unsigned events) noexcept; /** * Remove all sockets. @@ -203,6 +212,11 @@ public: i.ClearReturnedEvents(); } } + +#ifdef USE_EPOLL + for (const auto &i : always_ready_fds) + f(i.fd, i.revents); +#endif } protected: @@ -231,7 +245,6 @@ private: void OnTimeout() noexcept { SetReady(); - IdleMonitor::Schedule(); } virtual void OnIdle() noexcept final; diff --git a/src/event/SocketMonitor.cxx b/src/event/SocketMonitor.cxx index 76f550094..f62e81404 100644 --- a/src/event/SocketMonitor.cxx +++ b/src/event/SocketMonitor.cxx @@ -64,20 +64,24 @@ SocketMonitor::Close() noexcept Steal().Close(); } -void +bool SocketMonitor::Schedule(unsigned flags) noexcept { assert(IsDefined()); if (flags == GetScheduledFlags()) - return; + return true; + bool success; if (scheduled_flags == 0) - loop.AddFD(fd.Get(), flags, *this); + success = loop.AddFD(fd.Get(), flags, *this); else if (flags == 0) - loop.RemoveFD(fd.Get(), *this); + success = loop.RemoveFD(fd.Get(), *this); else - loop.ModifyFD(fd.Get(), flags, *this); + success = loop.ModifyFD(fd.Get(), flags, *this); - scheduled_flags = flags; + if (success) + scheduled_flags = flags; + + return success; } diff --git a/src/event/SocketMonitor.hxx b/src/event/SocketMonitor.hxx index 4f4aa82c3..3804bf4ab 100644 --- a/src/event/SocketMonitor.hxx +++ b/src/event/SocketMonitor.hxx @@ -98,18 +98,22 @@ public: return scheduled_flags; } - void Schedule(unsigned flags) noexcept; + /** + * @return true on success, false on error (with errno set if + * USE_EPOLL is defined) + */ + bool Schedule(unsigned flags) noexcept; void Cancel() noexcept { Schedule(0); } - void ScheduleRead() noexcept { - Schedule(GetScheduledFlags() | READ | HANGUP | ERROR); + bool ScheduleRead() noexcept { + return Schedule(GetScheduledFlags() | READ | HANGUP | ERROR); } - void ScheduleWrite() noexcept { - Schedule(GetScheduledFlags() | WRITE); + bool ScheduleWrite() noexcept { + return Schedule(GetScheduledFlags() | WRITE); } void CancelRead() noexcept { diff --git a/src/input/plugins/CurlInputPlugin.cxx b/src/input/plugins/CurlInputPlugin.cxx index f4f88a13b..8d6c0d9d3 100644 --- a/src/input/plugins/CurlInputPlugin.cxx +++ b/src/input/plugins/CurlInputPlugin.cxx @@ -180,7 +180,6 @@ CurlInputStream::FreeEasyIndirect() noexcept { BlockingCall(GetEventLoop(), [this](){ FreeEasy(); - (*curl_init)->InvalidateSockets(); }); } diff --git a/src/lib/curl/Global.cxx b/src/lib/curl/Global.cxx index 1e978bf7f..c83df1ad4 100644 --- a/src/lib/curl/Global.cxx +++ b/src/lib/curl/Global.cxx @@ -162,7 +162,6 @@ CurlGlobal::Remove(CurlRequest &r) noexcept assert(GetEventLoop().IsInside()); curl_multi_remove_handle(multi.Get(), r.Get()); - InvalidateSockets(); } /** @@ -220,12 +219,12 @@ CurlGlobal::UpdateTimeout(long timeout_ms) noexcept return; } - if (timeout_ms < 10) - /* CURL 7.21.1 likes to report "timeout=0", which + if (timeout_ms < 1) + /* CURL's threaded resolver sets a timeout of 0ms, which means we're running in a busy loop. Quite a bad idea to waste so much CPU. Let's use a lower limit - of 10ms. */ - timeout_ms = 10; + of 1ms. */ + timeout_ms = 1; timeout_event.Schedule(std::chrono::milliseconds(timeout_ms)); } diff --git a/src/lib/curl/Global.hxx b/src/lib/curl/Global.hxx index ccc180bcf..95065e9da 100644 --- a/src/lib/curl/Global.hxx +++ b/src/lib/curl/Global.hxx @@ -67,16 +67,6 @@ public: SocketAction(CURL_SOCKET_TIMEOUT, 0); } - /** - * This is a kludge to allow pausing/resuming a stream with - * libcurl < 7.32.0. Read the curl_easy_pause manpage for - * more information. - */ - void ResumeSockets() { - int running_handles; - curl_multi_socket_all(multi.Get(), &running_handles); - } - private: /** * Check for finished HTTP responses. diff --git a/src/lib/curl/Request.cxx b/src/lib/curl/Request.cxx index 8e576cd24..9126ee5c7 100644 --- a/src/lib/curl/Request.cxx +++ b/src/lib/curl/Request.cxx @@ -30,7 +30,6 @@ #include "config.h" #include "Request.hxx" #include "Global.hxx" -#include "Version.hxx" #include "Handler.hxx" #include "event/Call.hxx" #include "util/RuntimeError.hxx" @@ -122,12 +121,6 @@ CurlRequest::Resume() noexcept easy.Unpause(); - if (IsCurlOlderThan(0x072000)) - /* libcurl older than 7.32.0 does not update - its sockets after curl_easy_pause(); force - libcurl to do it now */ - global.ResumeSockets(); - global.InvalidateSockets(); } diff --git a/src/player/Thread.cxx b/src/player/Thread.cxx index c09a00eca..0e982f63f 100644 --- a/src/player/Thread.cxx +++ b/src/player/Thread.cxx @@ -46,6 +46,7 @@ #include "CrossFade.hxx" #include "tag/Tag.hxx" #include "Idle.hxx" +#include "util/Compiler.h" #include "util/Domain.hxx" #include "thread/Name.hxx" #include "Log.hxx" @@ -1175,6 +1176,7 @@ try { } /* fall through */ + gcc_fallthrough; case PlayerCommand::PAUSE: next_song.reset(); diff --git a/src/storage/plugins/CurlStorage.cxx b/src/storage/plugins/CurlStorage.cxx index 72ad439f4..af43eb876 100644 --- a/src/storage/plugins/CurlStorage.cxx +++ b/src/storage/plugins/CurlStorage.cxx @@ -105,7 +105,9 @@ public: BIND_THIS_METHOD(OnDeferredStart)), request(curl, uri, *this) { // TODO: use CurlInputStream's configuration + } + void DeferStart() noexcept { /* start the transfer inside the IOThread */ defer_start.Schedule(); } @@ -278,6 +280,7 @@ public: } using BlockingHttpRequest::GetEasy; + using BlockingHttpRequest::DeferStart; using BlockingHttpRequest::Wait; protected: @@ -425,6 +428,7 @@ public: } const StorageFileInfo &Perform() { + DeferStart(); Wait(); return info; } @@ -476,6 +480,7 @@ public: base_path(UriPathOrSlash(uri)) {} std::unique_ptr Perform() { + DeferStart(); Wait(); return ToReader(); } diff --git a/src/time/ISO8601.cxx b/src/time/ISO8601.cxx index 5cb4c486c..725215ea6 100644 --- a/src/time/ISO8601.cxx +++ b/src/time/ISO8601.cxx @@ -58,6 +58,8 @@ FormatISO8601(std::chrono::system_clock::time_point tp) return FormatISO8601(GmTime(tp)); } +#ifndef _WIN32 + static std::pair ParseTimeZoneOffsetRaw(const char *&s) { @@ -108,6 +110,67 @@ ParseTimeZoneOffset(const char *&s) return d; } +static const char * +ParseTimeOfDay(const char *s, struct tm &tm, + std::chrono::system_clock::duration &precision) noexcept +{ + /* this function always checks "end==s" to work around a + strptime() bug on OS X: if nothing could be parsed, + strptime() returns the input string (indicating success) + instead of nullptr (indicating error) */ + + const char *end = strptime(s, "%H", &tm); + if (end == nullptr || end == s) + return end; + + s = end; + precision = std::chrono::hours(1); + + if (*s == ':') { + /* with field separators: now a minute must follow */ + + ++s; + + end = strptime(s, "%M", &tm); + if (end == nullptr || end == s) + return nullptr; + + s = end; + precision = std::chrono::minutes(1); + + /* the "seconds" field is optional */ + if (*s != ':') + return s; + + ++s; + + end = strptime(s, "%S", &tm); + if (end == nullptr || end == s) + return nullptr; + + precision = std::chrono::seconds(1); + return end; + } + + /* without field separators */ + + end = strptime(s, "%M", &tm); + if (end == nullptr || end == s) + return s; + + s = end; + precision = std::chrono::minutes(1); + + end = strptime(s, "%S", &tm); + if (end == nullptr || end == s) + return s; + + precision = std::chrono::seconds(1); + return end; +} + +#endif + std::pair ParseISO8601(const char *s) @@ -138,22 +201,9 @@ ParseISO8601(const char *s) if (*s == 'T') { ++s; - if ((end = strptime(s, "%T", &tm)) != nullptr) - precision = std::chrono::seconds(1); - else if ((end = strptime(s, "%H%M%S", &tm)) != nullptr) - /* no field separators */ - precision = std::chrono::seconds(1); - else if ((end = strptime(s, "%H%M", &tm)) != nullptr) - /* no field separators */ - precision = std::chrono::minutes(1); - else if ((end = strptime(s, "%H:%M", &tm)) != nullptr) - precision = std::chrono::minutes(1); - else if ((end = strptime(s, "%H", &tm)) != nullptr) - precision = std::chrono::hours(1); - else + s = ParseTimeOfDay(s, tm, precision); + if (s == nullptr) throw std::runtime_error("Failed to parse time of day"); - - s = end; } auto tp = TimeGm(tm); diff --git a/src/util/Compiler.h b/src/util/Compiler.h index e441d3835..57a464a6e 100644 --- a/src/util/Compiler.h +++ b/src/util/Compiler.h @@ -143,6 +143,14 @@ #define gcc_flatten #endif +#if GCC_CHECK_VERSION(7,0) +#define gcc_fallthrough __attribute__((fallthrough)) +#elif CLANG_CHECK_VERSION(10,0) +#define gcc_fallthrough [[fallthrough]] +#else +#define gcc_fallthrough +#endif + #ifndef __cplusplus /* plain C99 has "restrict" */ #define gcc_restrict restrict diff --git a/src/util/format.c b/src/util/format.c index 1906e0d33..774e8a2e9 100644 --- a/src/util/format.c +++ b/src/util/format.c @@ -19,6 +19,7 @@ */ #include "format.h" +#include "util/Compiler.h" #include #include @@ -238,6 +239,7 @@ format_object2(const char *format, const char **last, const void *object, } /* fall through */ + gcc_fallthrough; default: /* pass-through non-escaped portions of the format string */ diff --git a/test/RunCurl.cxx b/test/RunCurl.cxx new file mode 100644 index 000000000..c8ca25909 --- /dev/null +++ b/test/RunCurl.cxx @@ -0,0 +1,93 @@ +/* + * Copyright 2003-2019 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "ShutdownHandler.hxx" +#include "lib/curl/Global.hxx" +#include "lib/curl/Request.hxx" +#include "lib/curl/Handler.hxx" +#include "event/Loop.hxx" +#include "util/PrintException.hxx" + +#include + +class MyHandler final : public CurlResponseHandler { + EventLoop &event_loop; + + std::exception_ptr error; + +public: + explicit MyHandler(EventLoop &_event_loop) noexcept + :event_loop(_event_loop) {} + + void Finish() { + if (error) + std::rethrow_exception(error); + } + + /* virtual methods from CurlResponseHandler */ + void OnHeaders(unsigned status, + std::multimap &&headers) override { + fprintf(stderr, "status: %u\n", status); + for (const auto &i : headers) + fprintf(stderr, "%s: %s\n", + i.first.c_str(), i.second.c_str()); + } + + void OnData(ConstBuffer data) override { + if (fwrite(data.data, data.size, 1, stdout) != 1) + throw std::runtime_error("Failed to write"); + } + + void OnEnd() override { + event_loop.Break(); + } + + void OnError(std::exception_ptr e) noexcept override { + error = std::move(e); + event_loop.Break(); + } +}; + +int +main(int argc, char **argv) noexcept +try { + if (argc != 2) { + fprintf(stderr, "Usage: RunCurl URI\n"); + return EXIT_FAILURE; + } + + const char *const uri = argv[1]; + + EventLoop event_loop; + const ShutdownHandler shutdown_handler(event_loop); + CurlGlobal curl_global(event_loop); + + MyHandler handler(event_loop); + CurlRequest request(curl_global, uri, handler); + request.Start(); + + event_loop.Run(); + + handler.Finish(); + + return EXIT_SUCCESS; +} catch (...) { + PrintException(std::current_exception()); + return EXIT_FAILURE; +} diff --git a/test/meson.build b/test/meson.build index bdd6d82c7..db78ada48 100644 --- a/test/meson.build +++ b/test/meson.build @@ -342,6 +342,18 @@ executable( ) if curl_dep.found() + executable( + 'RunCurl', + 'RunCurl.cxx', + 'ShutdownHandler.cxx', + '../src/Log.cxx', + '../src/LogBackend.cxx', + include_directories: inc, + dependencies: [ + curl_dep, + ], + ) + test('test_icy_parser', executable( 'test_icy_parser', 'test_icy_parser.cxx', diff --git a/test/run_storage.cxx b/test/run_storage.cxx index 67816d8f8..b27b1bbe6 100644 --- a/test/run_storage.cxx +++ b/test/run_storage.cxx @@ -90,6 +90,29 @@ Ls(Storage &storage, const char *path) return EXIT_SUCCESS; } +static int +Stat(Storage &storage, const char *path) +{ + const auto info = storage.GetInfo(path, false); + switch (info.type) { + case StorageFileInfo::Type::OTHER: + printf("other\n"); + break; + + case StorageFileInfo::Type::REGULAR: + printf("regular\n"); + break; + + case StorageFileInfo::Type::DIRECTORY: + printf("directory\n"); + break; + } + + printf("size: %llu\n", (unsigned long long)info.size); + + return EXIT_SUCCESS; +} + int main(int argc, char **argv) try { @@ -117,6 +140,18 @@ try { storage_uri); return Ls(*storage, path); + } else if (strcmp(command, "stat") == 0) { + if (argc != 4) { + fprintf(stderr, "Usage: run_storage stat URI PATH\n"); + return EXIT_FAILURE; + } + + const char *const path = argv[3]; + + auto storage = MakeStorage(io_thread.GetEventLoop(), + storage_uri); + + return Stat(*storage, path); } else { fprintf(stderr, "Unknown command\n"); return EXIT_FAILURE;