diff options
40 files changed, 2682 insertions, 377 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 8a35d95..8a1a6aa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,72 +1,88 @@ -project (indicator-display) -cmake_minimum_required (VERSION 2.8.9) +project(indicator-display LANGUAGES C CXX) +cmake_minimum_required(VERSION 2.8.9) -list (APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake) +list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake) -set (PROJECT_VERSION "14.10.0") -set (PACKAGE ${CMAKE_PROJECT_NAME}) -set (GETTEXT_PACKAGE indicator-display) -add_definitions (-DGETTEXT_PACKAGE="${GETTEXT_PACKAGE}" - -DGNOMELOCALEDIR="${CMAKE_INSTALL_FULL_LOCALEDIR}") +set(PACKAGE ${CMAKE_PROJECT_NAME}) +set(GETTEXT_PACKAGE indicator-display) +add_definitions( + -DGETTEXT_PACKAGE="${GETTEXT_PACKAGE}" + -DGNOMELOCALEDIR="${CMAKE_INSTALL_FULL_LOCALEDIR}" +) -set (SERVICE_LIB ${PACKAGE}) -set (SERVICE_EXEC "${PACKAGE}-service") +set(SERVICE_LIB ${PACKAGE}) +set(SERVICE_EXEC "${PACKAGE}-service") -option (enable_tests "Build the package's automatic tests." ON) -option (enable_lcov "Generate lcov code coverage reports." ON) +option(enable_tests "Build the package's automatic tests." ON) +option(enable_coverage "Generate code coverage reports." ON) ## ## GNU standard paths ## -include (GNUInstallDirs) -if (EXISTS "/etc/debian_version") # Workaround for libexecdir on debian - set (CMAKE_INSTALL_LIBEXECDIR "${CMAKE_INSTALL_LIBDIR}") - set (CMAKE_INSTALL_FULL_LIBEXECDIR "${CMAKE_INSTALL_FULL_LIBDIR}") -endif () -set (CMAKE_INSTALL_PKGLIBEXECDIR "${CMAKE_INSTALL_LIBEXECDIR}/${CMAKE_PROJECT_NAME}") -set (CMAKE_INSTALL_FULL_PKGLIBEXECDIR "${CMAKE_INSTALL_FULL_LIBEXECDIR}/${CMAKE_PROJECT_NAME}") + +include(GNUInstallDirs) +if(EXISTS "/etc/debian_version") # Workaround for libexecdir on debian + set(CMAKE_INSTALL_LIBEXECDIR "${CMAKE_INSTALL_LIBDIR}") + set(CMAKE_INSTALL_FULL_LIBEXECDIR "${CMAKE_INSTALL_FULL_LIBDIR}") +endif() +set(CMAKE_INSTALL_PKGLIBEXECDIR "${CMAKE_INSTALL_LIBEXECDIR}/${CMAKE_PROJECT_NAME}") +set(CMAKE_INSTALL_FULL_PKGLIBEXECDIR "${CMAKE_INSTALL_FULL_LIBEXECDIR}/${CMAKE_PROJECT_NAME}") ## ## Check for prerequisites ## -find_package (PkgConfig REQUIRED) +# threads... +set(THREADS_PREFER_PTHREAD_FLAG ON) +find_package(Threads REQUIRED) +if(${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION} LESS 3.1) + set(THREAD_LINK_LIBRARIES -pthread) +else() + set(THREAD_LINK_LIBRARIES Threads::Threads) # introduced in cmake 3.1 +endif() + +find_package(PkgConfig REQUIRED) -include (FindPkgConfig) -pkg_check_modules (SERVICE_DEPS REQUIRED - gio-unix-2.0>=2.36 - glib-2.0>=2.36) -include_directories (SYSTEM ${SERVICE_DEPS_INCLUDE_DIRS}) +# glib... +set(GLIB_MINIMUM 2.36) +pkg_check_modules(SERVICE_DEPS REQUIRED + gio-unix-2.0>=${GLIB_MINIMUM} + glib-2.0>=${GLIB_MINIMUM} + gudev-1.0 +) +include_directories (SYSTEM + ${SERVICE_DEPS_INCLUDE_DIRS} +) ## -## +## Compiler settings ## -set (CMAKE_INCLUDE_CURRENT_DIR OFF) -include_directories (${CMAKE_CURRENT_SOURCE_DIR}) +include_directories(${CMAKE_CURRENT_SOURCE_DIR}) # set the compiler warnings -if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") - set (CXX_WARNING_ARGS "${CXX_WARNING_ARGS} -Weverything -Wno-c++98-compat") -else() - set (CXX_WARNING_ARGS "${CXX_WARNING_ARGS} -Wall -Wextra -Wpedantic") +if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") + list(APPEND CXX_WARNING_ARGS -Weverything -Wno-c++98-compat -Wno-padded) +elseif("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") + list(APPEND CXX_WARNING_ARGS -Wall -Wextra -Wpedantic) endif() -set (CXX_WARNING_ARGS "${CXX_WARNING_ARGS} -Wno-missing-field-initializers") # GActionEntry -# testing & coverage -if (${enable_tests}) - set (GTEST_SOURCE_DIR /usr/src/gtest/src) - set (GTEST_INCLUDE_DIR ${GTEST_SOURCE_DIR}/..) - set (GTEST_LIBS -lpthread) - enable_testing () - if (${enable_lcov}) - include(GCov) - endif () -endif () +add_compile_options(-std=c++14 -fPIC -g) + +## +## Testing & Coverage +## + +if(${enable_tests}) + enable_testing() + if(${enable_coverage}) + include(EnableCoverageReport) + endif() +endif() -add_subdirectory (src) -add_subdirectory (data) -add_subdirectory (po) +add_subdirectory(src) +add_subdirectory(data) +add_subdirectory(po) if (${enable_tests}) - add_subdirectory (tests) + add_subdirectory(tests) endif () diff --git a/cmake/FindGMock.cmake b/cmake/FindGMock.cmake deleted file mode 100644 index 74a1c15..0000000 --- a/cmake/FindGMock.cmake +++ /dev/null @@ -1,10 +0,0 @@ -# Build with system gmock and embedded gtest -set (GMOCK_INCLUDE_DIRS "/usr/include/gmock/include" CACHE PATH "gmock source include directory") -set (GMOCK_SOURCE_DIR "/usr/src/gmock" CACHE PATH "gmock source directory") -set (GTEST_INCLUDE_DIRS "${GMOCK_SOURCE_DIR}/gtest/include" CACHE PATH "gtest source include directory") - -add_subdirectory(${GMOCK_SOURCE_DIR} "${CMAKE_CURRENT_BINARY_DIR}/gmock") - -set(GTEST_LIBRARIES gtest) -set(GTEST_MAIN_LIBRARIES gtest_main) -set(GMOCK_LIBRARIES gmock gmock_main) diff --git a/cmake/GCov.cmake b/cmake/GCov.cmake deleted file mode 100644 index 81c0c40..0000000 --- a/cmake/GCov.cmake +++ /dev/null @@ -1,51 +0,0 @@ -if (CMAKE_BUILD_TYPE MATCHES coverage) - set(GCOV_FLAGS "${GCOV_FLAGS} --coverage") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${GCOV_FLAGS}") - set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} ${GCOV_FLAGS}") - set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${GCOV_FLAGS}") - set(GCOV_LIBS ${GCOV_LIBS} gcov) - - find_program(GCOVR_EXECUTABLE gcovr HINTS ${GCOVR_ROOT} "${GCOVR_ROOT}/bin") - if (NOT GCOVR_EXECUTABLE) - message(STATUS "Gcovr binary was not found, can not generate XML coverage info.") - else () - message(STATUS "Gcovr found, can generate XML coverage info.") - add_custom_target (coverage-xml - WORKING_DIRECTORY ${CMAKE_BINARY_DIR} - COMMAND "${GCOVR_EXECUTABLE}" --exclude="test.*" -x -r "${CMAKE_SOURCE_DIR}" - --object-directory=${CMAKE_BINARY_DIR} -o coverage.xml) - endif() - - find_program(LCOV_EXECUTABLE lcov HINTS ${LCOV_ROOT} "${GCOVR_ROOT}/bin") - find_program(GENHTML_EXECUTABLE genhtml HINTS ${GENHTML_ROOT}) - if (NOT LCOV_EXECUTABLE) - message(STATUS "Lcov binary was not found, can not generate HTML coverage info.") - else () - if(NOT GENHTML_EXECUTABLE) - message(STATUS "Genthml binary not found, can not generate HTML coverage info.") - else() - message(STATUS "Lcov and genhtml found, can generate HTML coverage info.") - add_custom_target (coverage-html - WORKING_DIRECTORY ${CMAKE_BINARY_DIR} - COMMAND "${CMAKE_CTEST_COMMAND}" --force-new-ctest-process --verbose - COMMAND "${LCOV_EXECUTABLE}" --directory ${CMAKE_BINARY_DIR} --capture | ${CMAKE_SOURCE_DIR}/trim-lcov.py > dconf-lcov.info - COMMAND "${LCOV_EXECUTABLE}" -r dconf-lcov.info /usr/include/\\* -o nosys-lcov.info - COMMAND LANG=C "${GENHTML_EXECUTABLE}" --prefix ${CMAKE_BINARY_DIR} --output-directory lcov-html --legend --show-details nosys-lcov.info - COMMAND ${CMAKE_COMMAND} -E echo "" - COMMAND ${CMAKE_COMMAND} -E echo "file://${CMAKE_BINARY_DIR}/lcov-html/index.html" - COMMAND ${CMAKE_COMMAND} -E echo "") - #COMMAND "${LCOV_EXECUTABLE}" --directory ${CMAKE_BINARY_DIR} --capture --output-file coverage.info --no-checksum - #COMMAND "${GENHTML_EXECUTABLE}" --prefix ${CMAKE_BINARY_DIR} --output-directory coveragereport --title "Code Coverage" --legend --show-details coverage.info - #COMMAND ${CMAKE_COMMAND} -E echo "\\#define foo \\\"bar\\\"" - #) - endif() - endif() -endif() - - - #$(MAKE) $(AM_MAKEFLAGS) check - #lcov --directory $(top_builddir) --capture --test-name dconf | $(top_srcdir)/trim-lcov.py > dconf-lcov.info - #LANG=C genhtml --prefix $(top_builddir) --output-directory lcov-html --legend --show-details dconf-lcov.info - #@echo - #@echo " file://$(abs_top_builddir)/lcov-html/index.html" - #@echo diff --git a/cmake/GdbusCodegen.cmake b/cmake/GdbusCodegen.cmake deleted file mode 100644 index ddb2995..0000000 --- a/cmake/GdbusCodegen.cmake +++ /dev/null @@ -1,36 +0,0 @@ -cmake_minimum_required(VERSION 2.6) -if(POLICY CMP0011) - cmake_policy(SET CMP0011 NEW) -endif(POLICY CMP0011) - -find_program(GDBUS_CODEGEN NAMES gdbus-codegen DOC "gdbus-codegen executable") -if(NOT GDBUS_CODEGEN) - message(FATAL_ERROR "Excutable gdbus-codegen not found") -endif() - -macro(add_gdbus_codegen outfiles name prefix service_xml) - add_custom_command( - OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${name}.h" "${CMAKE_CURRENT_BINARY_DIR}/${name}.c" - COMMAND "${GDBUS_CODEGEN}" - --interface-prefix "${prefix}" - --generate-c-code "${name}" - "${service_xml}" - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} - DEPENDS ${ARGN} "${service_xml}" - ) - list(APPEND ${outfiles} "${CMAKE_CURRENT_BINARY_DIR}/${name}.c") -endmacro(add_gdbus_codegen) - -macro(add_gdbus_codegen_with_namespace outfiles name prefix namespace service_xml) - add_custom_command( - OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${name}.h" "${CMAKE_CURRENT_BINARY_DIR}/${name}.c" - COMMAND "${GDBUS_CODEGEN}" - --interface-prefix "${prefix}" - --generate-c-code "${name}" - --c-namespace "${namespace}" - "${service_xml}" - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} - DEPENDS ${ARGN} "${service_xml}" - ) - list(APPEND ${outfiles} "${CMAKE_CURRENT_BINARY_DIR}/${name}.c") -endmacro(add_gdbus_codegen_with_namespace) diff --git a/debian/control b/debian/control index 87b1bf8..90e2590 100644 --- a/debian/control +++ b/debian/control @@ -4,12 +4,18 @@ Priority: optional Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com> XSBC-Original-Maintainer: Charles Kerr <charles.kerr@canonical.com> Build-Depends: cmake, + cmake-extras (>= 0.4), dbus, libglib2.0-dev (>= 2.36), + libgudev-1.0-dev, libproperties-cpp-dev, # for coverage reports lcov, # for tests + qt5-default, + qtbase5-dev, + libqtdbusmock1-dev, + libqtdbustest1-dev, cppcheck, libgtest-dev, google-mock (>= 1.6.0+svn437), diff --git a/po/POTFILES.in b/po/POTFILES.in index f8bd80a..339765f 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -1 +1,2 @@ src/rotation-lock.cpp +src/usb-snap.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 982aa49..060071d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,29 +1,37 @@ -set (SERVICE_LIB "indicatordisplayservice") -set (SERVICE_EXEC "indicator-display-service") -add_definitions (-DG_LOG_DOMAIN="${CMAKE_PROJECT_NAME}") - -# handwritten source code... -set (SERVICE_LIB_HANDWRITTEN_SOURCES - exporter.cpp - rotation-lock.cpp) - -add_library (${SERVICE_LIB} STATIC - ${SERVICE_LIB_HANDWRITTEN_SOURCES}) - -# add the bin dir to the include path so that -# the compiler can find the generated header files -include_directories (${CMAKE_CURRENT_BINARY_DIR}) - -link_directories (${SERVICE_DEPS_LIBRARY_DIRS}) - -set (SERVICE_EXEC_HANDWRITTEN_SOURCES main.cpp) -add_executable (${SERVICE_EXEC} ${SERVICE_EXEC_HANDWRITTEN_SOURCES}) -target_link_libraries (${SERVICE_EXEC} ${SERVICE_LIB} ${SERVICE_DEPS_LIBRARIES} ${GCOV_LIBS}) -install (TARGETS ${SERVICE_EXEC} RUNTIME DESTINATION ${CMAKE_INSTALL_FULL_PKGLIBEXECDIR}) - -# add warnings/coverage info on handwritten files -# but not the generated ones... -set_property (SOURCE ${SERVICE_LIB_HANDWRITTEN_SOURCES} ${SERVICE_EXEC_HANDWRITTEN_SOURCES} - APPEND_STRING PROPERTY COMPILE_FLAGS " -std=c++11 -g ${CXX_WARNING_ARGS} ${GCOV_FLAGS}") +add_definitions(-DG_LOG_DOMAIN="${CMAKE_PROJECT_NAME}") + +add_compile_options( + ${CXX_WARNING_ARGS} +) + +add_library( + ${SERVICE_LIB} + STATIC + adbd-client.cpp + exporter.cpp + greeter.cpp + indicator.cpp + rotation-lock.cpp + usb-manager.cpp + usb-monitor.cpp + usb-snap.cpp +) + +add_executable( + ${SERVICE_EXEC} + main.cpp +) + +target_link_libraries(${SERVICE_EXEC} + ${SERVICE_LIB} + ${SERVICE_DEPS_LIBRARIES} + ${THREAD_LINK_LIBRARIES} +) + +install( + TARGETS + ${SERVICE_EXEC} + RUNTIME DESTINATION ${CMAKE_INSTALL_FULL_PKGLIBEXECDIR} +) diff --git a/src/adbd-client.cpp b/src/adbd-client.cpp new file mode 100644 index 0000000..400c7c9 --- /dev/null +++ b/src/adbd-client.cpp @@ -0,0 +1,303 @@ +/* + * Copyright 2016 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#include <src/adbd-client.h> + +#include <gio/gio.h> +#include <gio/gunixsocketaddress.h> + +#include <algorithm> +#include <cctype> +#include <cstring> +#include <chrono> +#include <condition_variable> +#include <mutex> +#include <thread> + +class GAdbdClient::Impl +{ +public: + + explicit Impl(const std::string& socket_path): + m_socket_path{socket_path}, + m_cancellable{g_cancellable_new()}, + m_worker_thread{&Impl::worker_func, this} + { + } + + ~Impl() + { + // tell the worker thread to stop whatever it's doing and exit. + g_debug("%s Client::Impl dtor, cancelling m_cancellable", G_STRLOC); + g_cancellable_cancel(m_cancellable); + m_pkresponse_cv.notify_one(); + m_sleep_cv.notify_one(); + m_worker_thread.join(); + g_clear_object(&m_cancellable); + } + + core::Signal<const PKRequest&>& on_pk_request() + { + return m_on_pk_request; + } + +private: + + // struct to carry request info from the worker thread to the GMainContext thread + struct PKIdleData + { + Impl* self = nullptr; + GCancellable* cancellable = nullptr; + const std::string public_key; + + PKIdleData(Impl* self_, GCancellable* cancellable_, std::string public_key_): + self(self_), + cancellable(G_CANCELLABLE(g_object_ref(cancellable_))), + public_key(public_key_) {} + + ~PKIdleData() {g_clear_object(&cancellable);} + + }; + + void pass_public_key_to_main_thread(const std::string& public_key) + { + g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, + on_public_key_request_static, + new PKIdleData{this, m_cancellable, public_key}, + [](gpointer id){delete static_cast<PKIdleData*>(id);}); + } + + static gboolean on_public_key_request_static (gpointer gdata) // runs in main thread + { + /* NB: It's possible (though unlikely) that data.self was destroyed + while this callback was pending, so we must check is-cancelled FIRST */ + auto data = static_cast<PKIdleData*>(gdata); + if (!g_cancellable_is_cancelled(data->cancellable)) + { + // notify our listeners of the request + auto self = data->self; + struct PKRequest req; + req.public_key = data->public_key; + req.fingerprint = get_fingerprint(req.public_key); + req.respond = [self](PKResponse response){self->on_public_key_response(response);}; + self->m_on_pk_request(req); + } + + return G_SOURCE_REMOVE; + } + + void on_public_key_response(PKResponse response) + { + // set m_pkresponse and wake up the waiting worker thread + std::unique_lock<std::mutex> lk(m_pkresponse_mutex); + m_pkresponse = response; + m_pkresponse_ready = true; + m_pkresponse_cv.notify_one(); + } + + /*** + **** + ***/ + + void worker_func() // runs in worker thread + { + const std::string socket_path {m_socket_path}; + + while (!g_cancellable_is_cancelled(m_cancellable)) + { + g_debug("%s creating a client socket to '%s'", G_STRLOC, socket_path.c_str()); + auto socket = create_client_socket(socket_path); + bool got_valid_req = false; + + g_debug("%s calling read_request", G_STRLOC); + std::string reqstr; + if (socket != nullptr) + reqstr = read_request(socket); + if (!reqstr.empty()) + g_debug("%s got request [%s]", G_STRLOC, reqstr.c_str()); + + if (reqstr.substr(0,2) == "PK") { + PKResponse response = PKResponse::DENY; + const auto public_key = reqstr.substr(2); + g_debug("%s got pk [%s]", G_STRLOC, public_key.c_str()); + if (!public_key.empty()) { + got_valid_req = true; + std::unique_lock<std::mutex> lk(m_pkresponse_mutex); + m_pkresponse_ready = false; + pass_public_key_to_main_thread(public_key); + m_pkresponse_cv.wait(lk, [this](){ + return m_pkresponse_ready || g_cancellable_is_cancelled(m_cancellable); + }); + response = m_pkresponse; + g_debug("%s got response '%d', is-cancelled %d", G_STRLOC, + int(response), + int(g_cancellable_is_cancelled(m_cancellable))); + } + if (!g_cancellable_is_cancelled(m_cancellable)) + send_pk_response(socket, response); + } else if (!reqstr.empty()) { + g_warning("Invalid ADB request: [%s]", reqstr.c_str()); + } + + g_clear_object(&socket); + + // If nothing interesting's happening, sleep a bit. + // (Interval copied from UsbDebuggingManager.java) + static constexpr std::chrono::seconds sleep_interval {std::chrono::seconds(1)}; + if (!got_valid_req && !g_cancellable_is_cancelled(m_cancellable)) { + std::unique_lock<std::mutex> lk(m_sleep_mutex); + m_sleep_cv.wait_for(lk, sleep_interval); + } + } + } + + // connect to a local domain socket + GSocket* create_client_socket(const std::string& socket_path) + { + GError* error {}; + auto socket = g_socket_new(G_SOCKET_FAMILY_UNIX, + G_SOCKET_TYPE_STREAM, + G_SOCKET_PROTOCOL_DEFAULT, + &error); + if (error != nullptr) { + g_warning("Error creating adbd client socket: %s", error->message); + g_clear_error(&error); + g_clear_object(&socket); + return nullptr; + } + + auto address = g_unix_socket_address_new(socket_path.c_str()); + const auto connected = g_socket_connect(socket, address, m_cancellable, &error); + g_clear_object(&address); + if (!connected) { + g_debug("unable to connect to '%s': %s", socket_path.c_str(), error->message); + g_clear_error(&error); + g_clear_object(&socket); + return nullptr; + } + + return socket; + } + + std::string read_request(GSocket* socket) + { + char buf[4096] = {}; + g_debug("%s calling g_socket_receive()", G_STRLOC); + const auto n_bytes = g_socket_receive (socket, buf, sizeof(buf), m_cancellable, nullptr); + std::string ret; + if (n_bytes > 0) + ret.append(buf, std::string::size_type(n_bytes)); + g_debug("%s g_socket_receive got %d bytes: [%s]", G_STRLOC, int(n_bytes), ret.c_str()); + return ret; + } + + void send_pk_response(GSocket* socket, PKResponse response) + { + std::string response_str; + switch(response) { + case PKResponse::ALLOW: response_str = "OK"; break; + case PKResponse::DENY: response_str = "NO"; break; + } + g_debug("%s sending reply: [%s]", G_STRLOC, response_str.c_str()); + + GError* error {}; + g_socket_send(socket, + response_str.c_str(), + response_str.size(), + m_cancellable, + &error); + if (error != nullptr) { + if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning("GAdbdServer: Error accepting socket connection: %s", error->message); + g_clear_error(&error); + } + } + + static std::string get_fingerprint(const std::string& public_key) + { + // The first token is base64-encoded data, so cut on the first whitespace + const std::string base64 ( + public_key.begin(), + std::find_if( + public_key.begin(), public_key.end(), + [](const std::string::value_type& ch){return std::isspace(ch);} + ) + ); + + gsize digest_len {}; + auto digest = g_base64_decode(base64.c_str(), &digest_len); + + auto checksum = g_compute_checksum_for_data(G_CHECKSUM_MD5, digest, digest_len); + const gsize checksum_len = checksum ? strlen(checksum) : 0; + + // insert ':' between character pairs; eg "ff27b5f3" --> "ff:27:b5:f3" + std::string fingerprint; + for (gsize i=0; i<checksum_len; ) { + fingerprint.append(checksum+i, checksum+i+2); + if (i < checksum_len-2) + fingerprint.append(":"); + i += 2; + } + + g_clear_pointer(&digest, g_free); + g_clear_pointer(&checksum, g_free); + return fingerprint; + } + + const std::string m_socket_path; + GCancellable* m_cancellable = nullptr; + std::thread m_worker_thread; + core::Signal<const PKRequest&> m_on_pk_request; + + std::mutex m_sleep_mutex; + std::condition_variable m_sleep_cv; + + std::mutex m_pkresponse_mutex; + std::condition_variable m_pkresponse_cv; + bool m_pkresponse_ready = false; + PKResponse m_pkresponse = PKResponse::DENY; +}; + +/*** +**** +***/ + +AdbdClient::~AdbdClient() +{ +} + +/*** +**** +***/ + +GAdbdClient::GAdbdClient(const std::string& socket_path): + impl{new Impl{socket_path}} +{ +} + +GAdbdClient::~GAdbdClient() +{ +} + +core::Signal<const AdbdClient::PKRequest&>& +GAdbdClient::on_pk_request() +{ + return impl->on_pk_request(); +} + diff --git a/src/adbd-client.h b/src/adbd-client.h new file mode 100644 index 0000000..dcee2f1 --- /dev/null +++ b/src/adbd-client.h @@ -0,0 +1,74 @@ +/* + * Copyright 2016 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#pragma once + +#include <functional> +#include <memory> +#include <string> + +#include <core/signal.h> + +/** + * Receives public key requests from ADBD and sends a response back. + * + * AdbClient only provides a receive/respond mechanism. The decision + * of what response gets sent is delegated out to a listener via + * the on_pk_request signal. + * + * The decider should connect to on_pk_request, listen for PKRequests, + * and call the request's `respond' method with the desired response. + */ +class AdbdClient +{ +public: + virtual ~AdbdClient(); + + enum class PKResponse { DENY, ALLOW }; + + struct PKRequest { + std::string public_key; + std::string fingerprint; + std::function<void(PKResponse)> respond; + }; + + virtual core::Signal<const PKRequest&>& on_pk_request() =0; + +protected: + AdbdClient() =default; +}; + +/** + * An AdbdClient designed to work with GLib's event loop. + * + * The on_pk_request() signal will be called in global GMainContext's thread; + * ie, just like a function passed to g_idle_add() or g_timeout_add(). + */ +class GAdbdClient: public AdbdClient +{ +public: + explicit GAdbdClient(const std::string& socket_path); + ~GAdbdClient(); + core::Signal<const PKRequest&>& on_pk_request() override; + +private: + class Impl; + std::unique_ptr<Impl> impl; +}; + diff --git a/src/dbus-names.h b/src/dbus-names.h new file mode 100644 index 0000000..b31098a --- /dev/null +++ b/src/dbus-names.h @@ -0,0 +1,60 @@ +/* + * Copyright 2016 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#pragma once + +namespace DBusNames +{ + namespace Notify + { + static constexpr char const * NAME = "org.freedesktop.Notifications"; + static constexpr char const * PATH = "/org/freedesktop/Notifications"; + static constexpr char const * INTERFACE = "org.freedesktop.Notifications"; + + namespace ActionInvoked + { + static constexpr char const * NAME = "ActionInvoked"; + } + + namespace NotificationClosed + { + static constexpr char const * NAME = "NotificationClosed"; + enum Reason { EXPIRED=1, DISMISSED=2, API=3, UNDEFINED=4 }; + } + } + + namespace UnityGreeter + { + static constexpr char const * NAME = "com.canonical.UnityGreeter"; + static constexpr char const * PATH = "/"; + static constexpr char const * INTERFACE = "com.canonical.UnityGreeter"; + } + + namespace Properties + { + static constexpr char const * INTERFACE = "org.freedesktop.DBus.Properties"; + + namespace PropertiesChanged + { + static constexpr char const* NAME = "PropertiesChanged"; + static constexpr char const* ARGS_VARIANT_TYPE = "(sa{sv}as)"; + } + } +} + diff --git a/src/greeter.cpp b/src/greeter.cpp new file mode 100644 index 0000000..f9cd965 --- /dev/null +++ b/src/greeter.cpp @@ -0,0 +1,161 @@ +/* + * Copyright 2016 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#include <src/dbus-names.h> +#include <src/greeter.h> + +#include <gio/gio.h> + +class UnityGreeter::Impl +{ +public: + + Impl(): + m_cancellable{g_cancellable_new()} + { + g_bus_get(G_BUS_TYPE_SESSION, m_cancellable, on_bus_ready_static, this); + } + + ~Impl() + { + g_cancellable_cancel(m_cancellable); + g_clear_object(&m_cancellable); + + if (m_subscription_id != 0) + g_dbus_connection_signal_unsubscribe (m_bus, m_subscription_id); + + g_clear_object(&m_bus); + } + + core::Property<bool>& is_active() + { + return m_is_active; + } + +private: + + static void on_bus_ready_static(GObject* /*source*/, GAsyncResult* res, gpointer gself) + { + GError* error {}; + auto bus = g_bus_get_finish (res, &error); + if (error != nullptr) { + if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning("UsbSnap: Error getting session bus: %s", error->message); + g_clear_error(&error); + } else { + static_cast<Impl*>(gself)->on_bus_ready(bus); + } + g_clear_object(&bus); + } + + void on_bus_ready(GDBusConnection* bus) + { + m_bus = G_DBUS_CONNECTION(g_object_ref(G_OBJECT(bus))); + + g_dbus_connection_call(m_bus, + DBusNames::UnityGreeter::NAME, + DBusNames::UnityGreeter::PATH, + DBusNames::Properties::INTERFACE, + "Get", + g_variant_new("(ss)", DBusNames::UnityGreeter::INTERFACE, "IsActive"), + G_VARIANT_TYPE("(v)"), + G_DBUS_CALL_FLAGS_NONE, + -1, + m_cancellable, + on_get_is_active_ready, + this); + + m_subscription_id = g_dbus_connection_signal_subscribe(m_bus, + DBusNames::UnityGreeter::NAME, + DBusNames::Properties::INTERFACE, + DBusNames::Properties::PropertiesChanged::NAME, + DBusNames::UnityGreeter::PATH, + DBusNames::UnityGreeter::INTERFACE, + G_DBUS_SIGNAL_FLAGS_NONE, + on_properties_changed_signal, + this, + nullptr); + } + + static void on_get_is_active_ready(GObject* source, GAsyncResult* res, gpointer gself) + { + GError* error {}; + auto v = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), res, &error); + if (error != nullptr) { + if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning("UsbSnap: Error getting session bus: %s", error->message); + g_clear_error(&error); + } else { + GVariant* is_active {}; + g_variant_get_child(v, 0, "v", &is_active); + static_cast<Impl*>(gself)->m_is_active.set(g_variant_get_boolean(is_active)); + g_clear_pointer(&is_active, g_variant_unref); + } + g_clear_pointer(&v, g_variant_unref); + } + + static void on_properties_changed_signal(GDBusConnection* /*connection*/, + const gchar* /*sender_name*/, + const gchar* object_path, + const gchar* interface_name, + const gchar* signal_name, + GVariant* parameters, + gpointer gself) + { + g_return_if_fail(!g_strcmp0(object_path, DBusNames::UnityGreeter::PATH)); + g_return_if_fail(!g_strcmp0(interface_name, DBusNames::Properties::INTERFACE)); + g_return_if_fail(!g_strcmp0(signal_name, DBusNames::Properties::PropertiesChanged::NAME)); + g_return_if_fail(g_variant_is_of_type(parameters, G_VARIANT_TYPE(DBusNames::Properties::PropertiesChanged::ARGS_VARIANT_TYPE))); + + auto v = g_variant_get_child_value (parameters, 1); + gboolean is_active {}; + if (g_variant_lookup(v, "IsActive", "b", &is_active)) + { + g_debug("%s is_active changed to %d", G_STRLOC, int(is_active)); + static_cast<Impl*>(gself)->m_is_active.set(is_active); + } + g_clear_pointer(&v, g_variant_unref); + } + + core::Property<bool> m_is_active; + GCancellable* m_cancellable {}; + GDBusConnection* m_bus {}; + unsigned int m_subscription_id {}; +}; + +/*** +**** +***/ + +Greeter::Greeter() =default; + +Greeter::~Greeter() =default; + +UnityGreeter::UnityGreeter(): + impl{new Impl{}} +{ +} + +UnityGreeter::~UnityGreeter() =default; + +core::Property<bool>& +UnityGreeter::is_active() +{ + return impl->is_active(); +} diff --git a/src/greeter.h b/src/greeter.h new file mode 100644 index 0000000..e084d25 --- /dev/null +++ b/src/greeter.h @@ -0,0 +1,47 @@ +/* + * Copyright 2016 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#pragma once + +#include <core/property.h> + +#include <memory> +#include <string> + +class Greeter +{ +public: + Greeter(); + virtual ~Greeter(); + virtual core::Property<bool>& is_active() =0; +}; + + +class UnityGreeter: public Greeter +{ +public: + UnityGreeter(); + virtual ~UnityGreeter(); + core::Property<bool>& is_active() override; + +protected: + class Impl; + std::unique_ptr<Impl> impl; +}; + diff --git a/src/indicator.cpp b/src/indicator.cpp new file mode 100644 index 0000000..77c4af7 --- /dev/null +++ b/src/indicator.cpp @@ -0,0 +1,37 @@ +/* + * Copyright 2016 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#include <src/indicator.h> + +Profile::Profile() +{ +} + +Profile::~Profile() +{ +} + +SimpleProfile::~SimpleProfile() +{ +} + +Indicator::~Indicator() +{ +} + diff --git a/src/indicator.h b/src/indicator.h index d0834fd..c55be79 100644 --- a/src/indicator.h +++ b/src/indicator.h @@ -1,5 +1,5 @@ /* - * Copyright 2014 Canonical Ltd. + * Copyright 2014-2016 Canonical Ltd. * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License version 3, as published @@ -17,8 +17,7 @@ * Charles Kerr <charles.kerr@canonical.com> */ -#ifndef INDICATOR_DISPLAY_INDICATOR_H -#define INDICATOR_DISPLAY_INDICATOR_H +#pragma once #include <core/property.h> @@ -52,10 +51,10 @@ public: virtual std::string name() const =0; virtual const core::Property<Header>& header() const =0; virtual std::shared_ptr<GMenuModel> menu_model() const =0; - virtual ~Profile() =default; + virtual ~Profile(); protected: - Profile() =default; + Profile(); }; @@ -63,6 +62,7 @@ class SimpleProfile: public Profile { public: SimpleProfile(const char* name, const std::shared_ptr<GMenuModel>& menu): m_name(name), m_menu(menu) {} + virtual ~SimpleProfile(); std::string name() const {return m_name;} core::Property<Header>& header() {return m_header;} @@ -79,11 +79,10 @@ protected: class Indicator { public: - virtual ~Indicator() =default; + virtual ~Indicator(); virtual const char* name() const =0; virtual GSimpleActionGroup* action_group() const =0; virtual std::vector<std::shared_ptr<Profile>> profiles() const =0; }; -#endif diff --git a/src/main.cpp b/src/main.cpp index 86bdeb3..52cdd58 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -20,6 +20,10 @@ #include <src/exporter.h> #include <src/rotation-lock.h> +#include <src/greeter.h> +#include <src/usb-manager.h> +#include <src/usb-monitor.h> + #include <glib/gi18n.h> // bindtextdomain() #include <gio/gio.h> @@ -54,6 +58,15 @@ main(int /*argc*/, char** /*argv*/) exporters.push_back(exporter); } + // We need the ADBD handler running, + // even though it doesn't have an indicator component yet + static constexpr char const * ADB_SOCKET_PATH {"/dev/socket/adbd"}; + static constexpr char const * PUBLIC_KEYS_FILENAME {"/data/misc/adb/adb_keys"}; + auto usb_monitor = std::make_shared<GUDevUsbMonitor>(); + auto greeter = std::make_shared<UnityGreeter>(); + UsbManager usb_manager {ADB_SOCKET_PATH, PUBLIC_KEYS_FILENAME, usb_monitor, greeter}; + + // let's go! g_main_loop_run(loop); // cleanup diff --git a/src/rotation-lock.cpp b/src/rotation-lock.cpp index f19ac9f..88c7e1b 100644 --- a/src/rotation-lock.cpp +++ b/src/rotation-lock.cpp @@ -43,6 +43,7 @@ public: ~Impl() { + g_signal_handlers_disconnect_by_data(m_settings, this); g_clear_object(&m_action_group); g_clear_object(&m_settings); } diff --git a/src/usb-manager.cpp b/src/usb-manager.cpp new file mode 100644 index 0000000..4d750c0 --- /dev/null +++ b/src/usb-manager.cpp @@ -0,0 +1,180 @@ +/* + * Copyright 2016 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#include <src/adbd-client.h> +#include <src/usb-manager.h> +#include <src/usb-snap.h> + +#include <glib.h> + +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <unistd.h> + +#include <set> + +class UsbManager::Impl +{ +public: + + explicit Impl( + const std::string& socket_path, + const std::string& public_keys_filename, + const std::shared_ptr<UsbMonitor>& usb_monitor, + const std::shared_ptr<Greeter>& greeter + ): + m_socket_path{socket_path}, + m_public_keys_filename{public_keys_filename}, + m_usb_monitor{usb_monitor}, + m_greeter{greeter} + { + m_usb_monitor->on_usb_disconnected().connect([this](const std::string& /*usb_name*/) { + restart(); + }); + + m_greeter->is_active().changed().connect([this](bool /*is_active*/) { + maybe_snap(); + }); + + restart(); + } + + ~Impl() + { + if (m_restart_idle_tag) + g_source_remove(m_restart_idle_tag); + + clear(); + } + +private: + + void clear() + { + // clear out old state + m_snap_connections.clear(); + m_snap.reset(); + m_req = decltype(m_req)(); + m_adbd_client.reset(); + } + + void restart() + { + clear(); + + // set a new client + m_adbd_client.reset(new GAdbdClient{m_socket_path}); + m_adbd_client->on_pk_request().connect( + [this](const AdbdClient::PKRequest& req) { + g_debug("%s got pk request: %s", G_STRLOC, req.fingerprint.c_str()); + m_req = req; + maybe_snap(); + } + ); + } + + void maybe_snap() + { + // don't prompt in the greeter! + if (!m_req.public_key.empty() && !m_greeter->is_active().get()) + snap(); + } + + void snap() + { + m_snap = std::make_shared<UsbSnap>(m_req.fingerprint); + m_snap_connections.insert((*m_snap).on_user_response().connect( + [this](AdbdClient::PKResponse response, bool remember_choice){ + g_debug("%s user responded! response %d, remember %d", G_STRLOC, int(response), int(remember_choice)); + m_req.respond(response); + if (remember_choice && (response == AdbdClient::PKResponse::ALLOW)) + write_public_key(m_req.public_key); + m_restart_idle_tag = g_idle_add([](gpointer gself){ + auto self = static_cast<Impl*>(gself); + self->m_restart_idle_tag = 0; + self->restart(); + return G_SOURCE_REMOVE; + }, this); + } + )); + } + + void write_public_key(const std::string& public_key) + { + g_debug("%s writing public key '%s' to '%s'", G_STRLOC, public_key.c_str(), m_public_keys_filename.c_str()); + + // confirm the directory exists + auto dirname = g_path_get_dirname(m_public_keys_filename.c_str()); + const auto dir_exists = g_file_test(dirname, G_FILE_TEST_IS_DIR); + if (!dir_exists) + g_warning("ADB data directory '%s' does not exist", dirname); + g_clear_pointer(&dirname, g_free); + if (!dir_exists) + return; + + // open the file in append mode, with user rw and group r permissions + const auto fd = open( + m_public_keys_filename.c_str(), + O_APPEND|O_CREAT|O_WRONLY, + S_IRUSR|S_IWUSR|S_IRGRP + ); + if (fd == -1) { + g_warning("Error opening ADB datafile: %s", g_strerror(errno)); + return; + } + + // write the new public key on its own line + std::string buf {public_key + '\n'}; + if (write(fd, buf.c_str(), buf.size()) == -1) + g_warning("Error writing ADB datafile: %d %s", errno, g_strerror(errno)); + close(fd); + } + + const std::string m_socket_path; + const std::string m_public_keys_filename; + const std::shared_ptr<UsbMonitor> m_usb_monitor; + const std::shared_ptr<Greeter> m_greeter; + + unsigned int m_restart_idle_tag {}; + + std::shared_ptr<GAdbdClient> m_adbd_client; + AdbdClient::PKRequest m_req; + std::shared_ptr<UsbSnap> m_snap; + std::set<core::ScopedConnection> m_snap_connections; +}; + +/*** +**** +***/ + +UsbManager::UsbManager( + const std::string& socket_path, + const std::string& public_keys_filename, + const std::shared_ptr<UsbMonitor>& usb_monitor, + const std::shared_ptr<Greeter>& greeter +): + impl{new Impl{socket_path, public_keys_filename, usb_monitor, greeter}} +{ +} + +UsbManager::~UsbManager() +{ +} + diff --git a/src/usb-manager.h b/src/usb-manager.h new file mode 100644 index 0000000..b93992f --- /dev/null +++ b/src/usb-manager.h @@ -0,0 +1,48 @@ +/* + * Copyright 2016 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#pragma once + +#include <src/greeter.h> +#include <src/usb-monitor.h> + +#include <memory> +#include <string> + +/** + * Manager class that connects the AdbdClient, UsbSnap, and manages the public key file + */ +class UsbManager +{ +public: + + UsbManager( + const std::string& socket_path, + const std::string& public_key_filename, + const std::shared_ptr<UsbMonitor>&, + const std::shared_ptr<Greeter>& + ); + + ~UsbManager(); + +protected: + + class Impl; + std::unique_ptr<Impl> impl; +}; diff --git a/src/usb-monitor.cpp b/src/usb-monitor.cpp new file mode 100644 index 0000000..5fc5a6d --- /dev/null +++ b/src/usb-monitor.cpp @@ -0,0 +1,81 @@ +/* + * Copyright 2016 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#include <src/usb-monitor.h> + +#include <glib.h> +#include <gudev/gudev.h> + +class GUDevUsbMonitor::Impl +{ +public: + + Impl() + { + const char* subsystems[] = {"android_usb", nullptr}; + m_udev_client = g_udev_client_new(subsystems); + g_signal_connect(m_udev_client, "uevent", G_CALLBACK(on_android_usb_event), this); + } + + ~Impl() + { + g_signal_handlers_disconnect_by_data(m_udev_client, this); + g_clear_object(&m_udev_client); + } + + core::Signal<const std::string&>& on_usb_disconnected() + { + return m_on_usb_disconnected; + } + +private: + + static void on_android_usb_event(GUdevClient*, gchar* action, GUdevDevice* device, gpointer gself) + { + if (!g_strcmp0(action, "change")) + if (!g_strcmp0(g_udev_device_get_property(device, "USB_STATE"), "DISCONNECTED")) + static_cast<Impl*>(gself)->m_on_usb_disconnected(g_udev_device_get_name(device)); + } + + core::Signal<const std::string&> m_on_usb_disconnected; + + GUdevClient* m_udev_client = nullptr; +}; + +/*** +**** +***/ + +UsbMonitor::UsbMonitor() =default; + +UsbMonitor::~UsbMonitor() =default; + +GUDevUsbMonitor::GUDevUsbMonitor(): + impl{new Impl{}} +{ +} + +GUDevUsbMonitor::~GUDevUsbMonitor() =default; + +core::Signal<const std::string&>& +GUDevUsbMonitor::on_usb_disconnected() +{ + return impl->on_usb_disconnected(); +} + diff --git a/src/usb-monitor.h b/src/usb-monitor.h new file mode 100644 index 0000000..d9be539 --- /dev/null +++ b/src/usb-monitor.h @@ -0,0 +1,52 @@ +/* + * Copyright 2016 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#pragma once + +#include <core/signal.h> + +#include <memory> +#include <string> + +/** + * Simple interface that emits signals on USB device state changes + */ +class UsbMonitor +{ +public: + UsbMonitor(); + virtual ~UsbMonitor(); + virtual core::Signal<const std::string&>& on_usb_disconnected() =0; +}; + +/** + * Simple GUDev wrapper that notifies on android_usb device state changes + */ +class GUDevUsbMonitor: public UsbMonitor +{ +public: + GUDevUsbMonitor(); + virtual ~GUDevUsbMonitor(); + core::Signal<const std::string&>& on_usb_disconnected() override; + +protected: + class Impl; + std::unique_ptr<Impl> impl; +}; + diff --git a/src/usb-snap.cpp b/src/usb-snap.cpp new file mode 100644 index 0000000..ba964fb --- /dev/null +++ b/src/usb-snap.cpp @@ -0,0 +1,250 @@ +/* + * Copyright 2016 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#include <src/dbus-names.h> +#include <src/usb-snap.h> + +#include <glib/gi18n.h> +#include <gio/gio.h> + +/*** +**** +***/ + +class UsbSnap::Impl +{ +public: + + explicit Impl(const std::string& fingerprint): + m_fingerprint{fingerprint}, + m_cancellable{g_cancellable_new()} + { + g_bus_get (G_BUS_TYPE_SESSION, m_cancellable, on_bus_ready_static, this); + } + + ~Impl() + { + g_cancellable_cancel(m_cancellable); + g_clear_object(&m_cancellable); + + if (m_subscription_id != 0) + g_dbus_connection_signal_unsubscribe (m_bus, m_subscription_id); + + if (m_notification_id != 0) { + GError* error {}; + g_dbus_connection_call_sync(m_bus, + DBusNames::Notify::NAME, + DBusNames::Notify::PATH, + DBusNames::Notify::INTERFACE, + "CloseNotification", + g_variant_new("(u)", m_notification_id), + nullptr, + G_DBUS_CALL_FLAGS_NONE, + -1, + nullptr, + &error); + if (error != nullptr) { + g_warning("Error closing notification: %s", error->message); + g_clear_error(&error); + } + } + + g_clear_object(&m_bus); + } + + core::Signal<AdbdClient::PKResponse,bool>& on_user_response() + { + return m_on_user_response; + } + +private: + + static void on_bus_ready_static(GObject* /*source*/, GAsyncResult* res, gpointer gself) + { + GError* error {}; + auto bus = g_bus_get_finish (res, &error); + if (error != nullptr) { + if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning("UsbSnap: Error getting session bus: %s", error->message); + g_clear_error(&error); + } else { + static_cast<Impl*>(gself)->on_bus_ready(bus); + } + g_clear_object(&bus); + } + + void on_bus_ready(GDBusConnection* bus) + { + m_bus = G_DBUS_CONNECTION(g_object_ref(G_OBJECT(bus))); + + m_subscription_id = g_dbus_connection_signal_subscribe(m_bus, + DBusNames::Notify::NAME, + DBusNames::Notify::INTERFACE, + nullptr, + DBusNames::Notify::PATH, + nullptr, + G_DBUS_SIGNAL_FLAGS_NONE, + on_notification_signal_static, + this, + nullptr); + + auto body = g_strdup_printf(_("The computer's RSA key fingerprint is: %s"), m_fingerprint.c_str()); + + GVariantBuilder actions_builder; + g_variant_builder_init(&actions_builder, G_VARIANT_TYPE_STRING_ARRAY); + g_variant_builder_add(&actions_builder, "s", ACTION_ALLOW); + g_variant_builder_add(&actions_builder, "s", _("Allow")); + g_variant_builder_add(&actions_builder, "s", ACTION_DENY); + g_variant_builder_add(&actions_builder, "s", _("Don't Allow")); + + GVariantBuilder hints_builder; + g_variant_builder_init(&hints_builder, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add(&hints_builder, "{sv}", "x-canonical-non-shaped-icon", g_variant_new_string("true")); + g_variant_builder_add(&hints_builder, "{sv}", "x-canonical-snap-decisions", g_variant_new_string("true")); + g_variant_builder_add(&hints_builder, "{sv}", "x-canonical-private-affirmative-tint", g_variant_new_string("true")); + + auto args = g_variant_new("(susssasa{sv}i)", + "", + uint32_t(0), + "computer-symbolic", + _("Allow USB Debugging?"), + body, + &actions_builder, + &hints_builder, + -1); + g_dbus_connection_call(m_bus, + DBusNames::Notify::NAME, + DBusNames::Notify::PATH, + DBusNames::Notify::INTERFACE, + "Notify", + args, + G_VARIANT_TYPE("(u)"), + G_DBUS_CALL_FLAGS_NONE, + -1, // timeout + m_cancellable, + on_notify_reply_static, + this); + + g_clear_pointer(&body, g_free); + } + + static void on_notify_reply_static(GObject* obus, GAsyncResult* res, gpointer gself) + { + GError* error {}; + auto reply = g_dbus_connection_call_finish (G_DBUS_CONNECTION(obus), res, &error); + if (error != nullptr) { + if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning("UsbSnap: Error calling Notify: %s", error->message); + g_clear_error(&error); + } else { + uint32_t id {}; + g_variant_get(reply, "(u)", &id); + static_cast<Impl*>(gself)->on_notify_reply(id); + } + g_clear_pointer(&reply, g_variant_unref); + } + + void on_notify_reply(uint32_t id) + { + m_notification_id = id; + } + + static void on_notification_signal_static(GDBusConnection* /*connection*/, + const gchar* /*sender_name*/, + const gchar* object_path, + const gchar* interface_name, + const gchar* signal_name, + GVariant* parameters, + gpointer gself) + { + g_return_if_fail(!g_strcmp0(object_path, DBusNames::Notify::PATH)); + g_return_if_fail(!g_strcmp0(interface_name, DBusNames::Notify::INTERFACE)); + + auto self = static_cast<Impl*>(gself); + + if (!g_strcmp0(signal_name, DBusNames::Notify::ActionInvoked::NAME)) + { + uint32_t id {}; + const char* action_name {}; + g_variant_get(parameters, "(u&s)", &id, &action_name); + if (id == self->m_notification_id) + self->on_action_invoked(action_name); + } + else if (!g_strcmp0(signal_name, DBusNames::Notify::NotificationClosed::NAME)) + { + uint32_t id {}; + uint32_t close_reason {}; + g_variant_get(parameters, "(uu)", &id, &close_reason); + if (id == self->m_notification_id) + self->on_notification_closed(close_reason); + } + } + + void on_action_invoked(const char* action_name) + { + const auto response = !g_strcmp0(action_name, ACTION_ALLOW) + ? AdbdClient::PKResponse::ALLOW + : AdbdClient::PKResponse::DENY; + + // FIXME: the current default is to cover the most common use case. + // We need to get the notification ui's checkbox working ASAP so + // that the user can provide this flag + const bool remember_this_choice = response == AdbdClient::PKResponse::ALLOW; + + m_on_user_response(response, remember_this_choice); + } + + void on_notification_closed(uint32_t close_reason) + { + if (close_reason == DBusNames::Notify::NotificationClosed::Reason::EXPIRED) + m_on_user_response(AdbdClient::PKResponse::DENY, false); + + m_notification_id = 0; + } + + static constexpr char const * ACTION_ALLOW {"allow"}; + static constexpr char const * ACTION_DENY {"deny"}; + + const std::string m_fingerprint; + core::Signal<AdbdClient::PKResponse,bool> m_on_user_response; + GCancellable* m_cancellable {}; + GDBusConnection* m_bus {}; + uint32_t m_notification_id {}; + unsigned int m_subscription_id {}; +}; + +/*** +**** +***/ + +UsbSnap::UsbSnap(const std::string& public_key): + impl{new Impl{public_key}} +{ +} + +UsbSnap::~UsbSnap() +{ +} + +core::Signal<AdbdClient::PKResponse,bool>& +UsbSnap::on_user_response() +{ + return impl->on_user_response(); +} + diff --git a/src/usb-snap.h b/src/usb-snap.h new file mode 100644 index 0000000..94de394 --- /dev/null +++ b/src/usb-snap.h @@ -0,0 +1,42 @@ +/* + * Copyright 2016 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#pragma once + +#include <src/adbd-client.h> // AdbdClient::PKResponse + +#include <core/signal.h> + +#include <memory> +#include <string> + +/** + * A snap decision prompt for whether or not to allow an ADB connection + */ +class UsbSnap +{ +public: + explicit UsbSnap(const std::string& public_key); + ~UsbSnap(); + core::Signal<AdbdClient::PKResponse,bool>& on_user_response(); + +protected: + class Impl; + std::unique_ptr<Impl> impl; +}; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 054a676..7be2acd 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,35 +1,35 @@ -include(FindGMock) -include_directories(${CMAKE_CURRENT_SOURCE_DIR}) -include_directories(${GMOCK_INCLUDE_DIRS}) -include_directories(${GTEST_INCLUDE_DIRS}) +set(CMAKE_AUTOMOC ON) +find_package(GMock REQUIRED) +find_package(Qt5Core REQUIRED) +find_package(Qt5Test REQUIRED) +find_package(Qt5DBus COMPONENTS Qt5DBusMacros REQUIRED) -# build libgtest -#add_library (gtest STATIC -# ${GTEST_SOURCE_DIR}/gtest-all.cc -# ${GTEST_SOURCE_DIR}/gtest_main.cc) -#set_target_properties (gtest PROPERTIES INCLUDE_DIRECTORIES ${INCLUDE_DIRECTORIES} ${GTEST_INCLUDE_DIR}) -#set_target_properties (gtest PROPERTIES COMPILE_FLAGS ${COMPILE_FLAGS} -w) +pkg_check_modules(TEST_DEPS + libqtdbustest-1 REQUIRED + libqtdbusmock-1 REQUIRED +) -if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") - # turn off the warnings that break Google Test - set (CXX_WARNING_ARGS "${CXX_WARNING_ARGS} -Wno-global-constructors -Wno-weak-vtables -Wno-undef -Wno-c++98-compat-pedantic -Wno-missing-noreturn -Wno-used-but-marked-unused -Wno-padded -Wno-deprecated -Wno-sign-compare -Wno-shift-sign-overflow") -endif() +include_directories(SYSTEM + ${DBUSTEST_INCLUDE_DIRS} + ${TEST_DEPS_INCLUDE_DIRS} + ${GTEST_INCLUDE_DIRS} + ${GMOCK_INCLUDE_DIRS} +) -SET (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -g ${CXX_WARNING_ARGS}") +list(APPEND CTEST_ENVIRONMENT + G_MESSAGES_DEBUG=all + G_DBUS_DEBUG=call,signal,return,message +) -# look for headers in our src dir, and also in the directories where we autogenerate files... -include_directories (${CMAKE_SOURCE_DIR}/src) -include_directories (${CMAKE_CURRENT_BINARY_DIR}) -include_directories (${DBUSTEST_INCLUDE_DIRS}) +# turn off the warnings that break Google Test +if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") + list(APPEND CXX_WARNING_ARGS -Wno-global-constructors -Wno-weak-vtables) +endif() -function(add_test_by_name name) - set (TEST_NAME ${name}) - add_executable (${TEST_NAME} ${TEST_NAME}.cpp) - add_test (${TEST_NAME} ${TEST_NAME}) - add_dependencies (${TEST_NAME} libindicatordisplayservice) - target_link_libraries (${TEST_NAME} indicatordisplayservice ${SERVICE_DEPS_LIBRARIES} ${GTEST_LIBRARIES} ${GMOCK_LIBRARIES}) -endfunction() -add_test_by_name(test-rotation-lock) +add_compile_options(${CXX_WARNING_ARGS}) -add_test (cppcheck cppcheck --enable=all -q --error-exitcode=2 --inline-suppr -I${CMAKE_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/tests) +add_test(cppcheck cppcheck --enable=all -USCHEMA_DIR --error-exitcode=2 --inline-suppr -I${CMAKE_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/tests) +add_subdirectory(integration) +add_subdirectory(unit) +add_subdirectory(utils) diff --git a/tests/glib-fixture.h b/tests/glib-fixture.h deleted file mode 100644 index 65d2921..0000000 --- a/tests/glib-fixture.h +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright 2014 Canonical Ltd. - * - * This program is free software: you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 3, as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranties of - * MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>. - * - * Authors: - * Charles Kerr <charles.kerr@canonical.com> - */ - -#ifndef INDICATOR_TESTS_GLIB_FIXTURE_H -#define INDICATOR_TESTS_GLIB_FIXTURE_H - -#include <map> - -#include <glib.h> -#include <glib/gstdio.h> -#include <gio/gio.h> - -#include <gtest/gtest.h> - -#include <locale.h> // setlocale() - -class GlibFixture : public ::testing::Test -{ - private: - - GLogFunc realLogHandler; - - std::map<GLogLevelFlags,size_t> expected_log; - std::map<GLogLevelFlags,std::vector<std::string>> log; - - void test_log_counts() - { - const GLogLevelFlags levels_to_test[] = { G_LOG_LEVEL_ERROR, - G_LOG_LEVEL_CRITICAL, - G_LOG_LEVEL_MESSAGE, - G_LOG_LEVEL_WARNING }; - - for(const auto& level : levels_to_test) - { - const auto& v = log[level]; - const auto n = v.size(); - - EXPECT_EQ(expected_log[level], n); - - if (expected_log[level] != n) - for (size_t i=0; i<n; ++i) - g_print("%d %s\n", (n+1), v[i].c_str()); - } - - expected_log.clear(); - log.clear(); - } - - static void default_log_handler(const gchar * log_domain, - GLogLevelFlags log_level, - const gchar * message, - gpointer self) - { - char* tmp = g_strdup_printf ("%s:%d \"%s\"", log_domain, (int)log_level, message); - static_cast<GlibFixture*>(self)->log[log_level].push_back(tmp); - g_free(tmp); - } - - protected: - - void increment_expected_errors(GLogLevelFlags level, size_t n=1) - { - expected_log[level] += n; - } - - virtual void SetUp() - { - setlocale(LC_ALL, "C.UTF-8"); - - loop = g_main_loop_new(nullptr, false); - - g_log_set_default_handler(default_log_handler, this); - - g_assert(g_setenv("GSETTINGS_BACKEND", "memory", true)); - - g_unsetenv("DISPLAY"); - } - - virtual void TearDown() - { - test_log_counts(); - - g_log_set_default_handler(realLogHandler, this); - - g_clear_pointer(&loop, g_main_loop_unref); - } - - private: - - static gboolean - wait_for_signal__timeout(gpointer name) - { - g_error("%s: timed out waiting for signal '%s'", G_STRLOC, (char*)name); - return G_SOURCE_REMOVE; - } - - static gboolean - wait_msec__timeout(gpointer loop) - { - g_main_loop_quit(static_cast<GMainLoop*>(loop)); - return G_SOURCE_CONTINUE; - } - - protected: - - /* convenience func to loop while waiting for a GObject's signal */ - void wait_for_signal(gpointer o, const gchar * signal, const guint timeout_seconds=5) - { - // wait for the signal or for timeout, whichever comes first - const auto handler_id = g_signal_connect_swapped(o, signal, - G_CALLBACK(g_main_loop_quit), - loop); - const auto timeout_id = g_timeout_add_seconds(timeout_seconds, - wait_for_signal__timeout, - loop); - g_main_loop_run(loop); - g_source_remove(timeout_id); - g_signal_handler_disconnect(o, handler_id); - } - - /* convenience func to loop for N msec */ - void wait_msec(guint msec=50) - { - const auto id = g_timeout_add(msec, wait_msec__timeout, loop); - g_main_loop_run(loop); - g_source_remove(id); - } - - GMainLoop * loop; - - public: - - virtual ~GlibFixture() =default; -}; - -#endif /* INDICATOR_TESTS_GLIB_FIXTURE_H */ diff --git a/tests/integration/CMakeLists.txt b/tests/integration/CMakeLists.txt new file mode 100644 index 0000000..9ec6688 --- /dev/null +++ b/tests/integration/CMakeLists.txt @@ -0,0 +1,24 @@ +set(SERVICE_LINK_LIBRARIES + ${SERVICE_LIB} + ${SERVICE_DEPS_LIBRARIES} +) +set(QT_LINK_LIBRARIES + test-utils + Qt5::Core + Qt5::Test + Qt5::DBus +) +set(TEST_LINK_LIBRARIES + ${TEST_DEPS_LIBRARIES} + ${GTEST_LIBRARIES} + ${GMOCK_LIBRARIES} +) + +function(add_qt_test_by_name name) + set(TEST_NAME ${name}) + add_executable (${TEST_NAME} ${TEST_NAME}.cpp) + add_test(${TEST_NAME} ${TEST_NAME}) + set_property(TEST ${TEST_NAME} APPEND PROPERTY ENVIRONMENT ${CTEST_ENVIRONMENT}) + target_link_libraries(${TEST_NAME} ${SERVICE_LINK_LIBRARIES} ${QT_LINK_LIBRARIES} ${TEST_LINK_LIBRARIES} ${THREAD_LINK_LIBRARIES}) +endfunction() +add_qt_test_by_name(usb-manager-test) diff --git a/tests/integration/usb-manager-test.cpp b/tests/integration/usb-manager-test.cpp new file mode 100644 index 0000000..d62756f --- /dev/null +++ b/tests/integration/usb-manager-test.cpp @@ -0,0 +1,226 @@ +/* + * Copyright 2016 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#include <tests/utils/adbd-server.h> +#include <tests/utils/qt-fixture.h> +#include <tests/utils/mock-greeter.h> +#include <tests/utils/mock-usb-monitor.h> + +#include <src/dbus-names.h> +#include <src/usb-manager.h> + +#include <libqtdbustest/DBusTestRunner.h> +#include <libqtdbustest/QProcessDBusService.h> +#include <libqtdbusmock/DBusMock.h> + +#include <fstream> +#include <sstream> +#include <vector> + +/*** +**** +***/ + +class UsbManagerFixture: public QtFixture +{ + using super = QtFixture; + +public: + + UsbManagerFixture(): + dbusMock{dbusTestRunner} + { + dbusTestRunner.startServices(); + } + + ~UsbManagerFixture() =default; + +protected: + + static void file_deleter (std::string* s) + { + fprintf(stderr, "remove \"%s\"\n", s->c_str()); + g_remove(s->c_str()); + delete s; + } + + void SetUp() override + { + super::SetUp(); + + m_usb_monitor.reset(new MockUsbMonitor{}); + m_greeter.reset(new MockGreeter{}); + + char tmpl[] = {"usb-manager-test-XXXXXX"}; + m_tmpdir.reset(new std::string{g_mkdtemp(tmpl)}, file_deleter); + g_message("using tmpdir '%s'", m_tmpdir->c_str()); + + dbusMock.registerNotificationDaemon(); + dbusTestRunner.startServices(); + } + + OrgFreedesktopDBusMockInterface& notificationsMockInterface() + { + return dbusMock.mockInterface(DBusNames::Notify::NAME, + DBusNames::Notify::PATH, + DBusNames::Notify::INTERFACE, + QDBusConnection::SessionBus); + } + + QtDBusTest::DBusTestRunner dbusTestRunner; + QtDBusMock::DBusMock dbusMock; + std::shared_ptr<std::string> m_tmpdir; + std::shared_ptr<MockUsbMonitor> m_usb_monitor; + std::shared_ptr<MockGreeter> m_greeter; +}; + +TEST_F(UsbManagerFixture, Allow) +{ + const std::shared_ptr<std::string> socket_path {new std::string{*m_tmpdir+"/socket"}, file_deleter}; + const std::shared_ptr<std::string> public_keys_path {new std::string{*m_tmpdir+"/adb_keys"}, file_deleter}; + + // add a signal spy to listen to the notification daemon + QSignalSpy notificationsSpy( + ¬ificationsMockInterface(), + SIGNAL(MethodCalled(const QString &, const QVariantList &)) + ); + + // start a mock AdbdServer ready to submit a request + const std::string public_key {"qAAAALUHllFjEZjl5jbS9ivjpQpaTNpibl28Re71D/S8sV3usNJTkbpvZYoVPfxtmHSNdCgLkWN6qcDZsHZqE/4myzmx/8Y/RqBy1oirudugi3YUUcJh7aWkY8lKQe9shCLTcrT7cFLZIJIidTvfmWTm0UcU+xmdPALze11I3lGo1Ty5KpCe9oP+qYM8suHbxhm78LKLlo0QJ2QqM8T5isr1pvoPHDgRb+mSESElG+xDIfPWA2BTu77/xk4EnXmOYfcuCr5akF3N4fRo/ACnYgXWDZFX2XdklBXyDj78lVlinF37xdMk7BMQh166X7UNkpH1uG2y5F6lUzyLg8SsFtRnJkw7eVe/gnJj3feQaFQbF5oVDhWhLMtWLtejhX6umvroVBVA4rynG4xEgs00K4u4ly8DUIIJYDO22Ml4myFR5CUm3lOlyitNdzYGh0utLXPq9oc8EbMVxM3i+O7PRxQw5Ul04X6K8GLiGUDV98DB+xYUqfEveq1BRnXi/ZrdPDhQ8Lfkg5xnLccPTFamAqutPtZXV6s7dXJInBTZf0NtBaWL0RdR2cOJBrpeBYkrc9yIyeqFLFdxr66rjaehjaa4pS4S+CD6PkGiIpPWSQtwNC4RlT10qTQ0/K9lRux2p0D8Z8ubUTFuh4kBScGUkN1OV3Z+7d7B+ghmBtZrrgleXsbehjRuKgEAAQA= foo@bar"}; + const std::string fingerprint {"12:23:5f:2d:8c:40:ae:1d:05:7b:ae:bd:88:8a:f0:80"}; + auto adbd_server = std::make_shared<GAdbdServer>(*socket_path, std::vector<std::string>{"PK"+public_key}); + + // set up a UsbManager to process the request + auto usb_manager = std::make_shared<UsbManager>(*socket_path, *public_keys_path, m_usb_monitor, m_greeter); + + // wait for the notification to show up, confirm it looks right + wait_for_signals(notificationsSpy, 1); + { + QVariantList const& call(notificationsSpy.at(0)); + EXPECT_EQ("Notify", call.at(0)); + + QVariantList const& args(call.at(1).toList()); + ASSERT_EQ(8, args.size()); + EXPECT_EQ("", args.at(0)); // app name + EXPECT_EQ(0, args.at(1)); // replaces-id + EXPECT_EQ("computer-symbolic", args.at(2)); // icon name + EXPECT_EQ("Allow USB Debugging?", args.at(3)); // summary + EXPECT_EQ(QString::fromUtf8("The computer's RSA key fingerprint is: ") + QString::fromUtf8(fingerprint.c_str()), args.at(4)); // body + EXPECT_EQ(QStringList({"allow", "Allow", "deny", "Don't Allow"}), args.at(5)); // actions + EXPECT_EQ(-1, args.at(7)); + + QVariantMap hints; + ASSERT_TRUE(qDBusArgumentToMap(args.at(6), hints)); + ASSERT_EQ(3, hints.size()); + ASSERT_TRUE(hints.contains("x-canonical-private-affirmative-tint")); + ASSERT_TRUE(hints.contains("x-canonical-non-shaped-icon")); + ASSERT_TRUE(hints.contains("x-canonical-snap-decisions")); + } + notificationsSpy.clear(); + + // click on allow in the notification + notificationsMockInterface().EmitSignal( + DBusNames::Notify::INTERFACE, + DBusNames::Notify::ActionInvoked::NAME, + "us", + QVariantList() << uint32_t(1) << "allow" + ); + + // confirm that the AdbdServer got the right response + wait_for([adbd_server](){return !adbd_server->m_responses.empty();}, 2000); + ASSERT_EQ(1, adbd_server->m_responses.size()); + EXPECT_EQ("OK", adbd_server->m_responses.front()); + + // confirm that the public_keys file got the public key appended to it + std::ifstream ifkeys {*public_keys_path}; + std::vector<std::string> lines; + std::string line; + while(getline(ifkeys, line)) + lines.emplace_back(std::move(line)); + ASSERT_EQ(1, lines.size()); + EXPECT_EQ(public_key, lines[0]); +} + +TEST_F(UsbManagerFixture, USBDisconnectedDuringPrompt) +{ + const std::shared_ptr<std::string> socket_path {new std::string{*m_tmpdir+"/socket"}, file_deleter}; + const std::shared_ptr<std::string> public_keys_path {new std::string{*m_tmpdir+"/adb_keys"}, file_deleter}; + + // start a mock AdbdServer ready to submit a request + const std::string public_key {"public_key"}; + auto adbd_server = std::make_shared<GAdbdServer>(*socket_path, std::vector<std::string>{"PK"+public_key}); + + // set up a UsbManager to process the request + auto usb_manager = std::make_shared<UsbManager>(*socket_path, *public_keys_path, m_usb_monitor, m_greeter); + + for (int i=0; i<3; i++) + { + // add a signal spy to listen to the notification daemon + QSignalSpy notificationsSpy( + ¬ificationsMockInterface(), + SIGNAL(MethodCalled(const QString &, const QVariantList &)) + ); + + // wait for a notification to show up + wait_for_signals(notificationsSpy, 1); + EXPECT_EQ("Notify", notificationsSpy.at(0).at(0)); + notificationsSpy.clear(); + + // wait for UsbSnap to receive dbusmock's response to the Notify request. + // there's no event to key off of for this, so just wait for a moment + wait_msec(); + + // disconnect the USB before the user has a chance to allow/deny + m_usb_monitor->m_on_usb_disconnected("android0"); + + // confirm that we requested the notification to be pulled down + wait_for_signals(notificationsSpy, 1); + EXPECT_EQ("CloseNotification", notificationsSpy.at(0).at(0)); + notificationsSpy.clear(); + } +} + +TEST_F(UsbManagerFixture, Greeter) +{ + const std::shared_ptr<std::string> socket_path {new std::string{*m_tmpdir+"/socket"}, file_deleter}; + const std::shared_ptr<std::string> public_keys_path {new std::string{*m_tmpdir+"/adb_keys"}, file_deleter}; + + // start a mock AdbdServer ready to submit a request + const std::string public_key {"public_key"}; + auto adbd_server = std::make_shared<GAdbdServer>(*socket_path, std::vector<std::string>{"PK"+public_key}); + + // set up a UsbManager to process the request + m_greeter->m_is_active.set(true); + auto usb_manager = std::make_shared<UsbManager>(*socket_path, *public_keys_path, m_usb_monitor, m_greeter); + + // add a signal spy to listen to the notification daemon + QSignalSpy notificationsSpy( + ¬ificationsMockInterface(), + SIGNAL(MethodCalled(const QString &, const QVariantList &)) + ); + + // the greeter is active, so the notification should not appear + EXPECT_FALSE(notificationsSpy.wait(2000)); + + // disable the greeter, the notification should appear + m_greeter->m_is_active.set(false); + wait_for_signals(notificationsSpy, 1); + EXPECT_EQ("Notify", notificationsSpy.at(0).at(0)); + notificationsSpy.clear(); +} diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt new file mode 100644 index 0000000..fe70461 --- /dev/null +++ b/tests/unit/CMakeLists.txt @@ -0,0 +1,34 @@ +set(SERVICE_LINK_LIBRARIES + ${SERVICE_LIB} + ${SERVICE_DEPS_LIBRARIES} +) +set(QT_LINK_LIBRARIES + test-utils + Qt5::Core + Qt5::Test + Qt5::DBus +) +set(TEST_LINK_LIBRARIES + ${TEST_DEPS_LIBRARIES} + ${GTEST_LIBRARIES} + ${GMOCK_LIBRARIES} +) + +function(add_test_by_name name) + set(TEST_NAME ${name}) + add_executable (${TEST_NAME} ${TEST_NAME}.cpp) + add_test(${TEST_NAME} ${TEST_NAME}) + set_property(TEST ${TEST_NAME} APPEND PROPERTY ENVIRONMENT ${CTEST_ENVIRONMENT}) + target_link_libraries(${TEST_NAME} ${SERVICE_LINK_LIBRARIES} ${TEST_LINK_LIBRARIES} ${THREAD_LINK_LIBRARIES}) +endfunction() +add_test_by_name(adbd-client-test) +add_test_by_name(rotation-lock-test) + +function(add_qt_test_by_name name) + set(TEST_NAME ${name}) + add_executable (${TEST_NAME} ${TEST_NAME}.cpp) + add_test(${TEST_NAME} ${TEST_NAME}) + set_property(TEST ${TEST_NAME} APPEND PROPERTY ENVIRONMENT ${CTEST_ENVIRONMENT}) + target_link_libraries(${TEST_NAME} ${SERVICE_LINK_LIBRARIES} ${QT_LINK_LIBRARIES} ${TEST_LINK_LIBRARIES} ${THREAD_LINK_LIBRARIES}) +endfunction() +add_qt_test_by_name(usb-snap-test) diff --git a/tests/unit/adbd-client-test.cpp b/tests/unit/adbd-client-test.cpp new file mode 100644 index 0000000..754f76c --- /dev/null +++ b/tests/unit/adbd-client-test.cpp @@ -0,0 +1,95 @@ +/* + * Copyright 2016 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#include <tests/utils/test-dbus-fixture.h> +#include <tests/utils/adbd-server.h> + +#include <src/adbd-client.h> + +class AdbdClientFixture: public TestDBusFixture +{ +private: + typedef TestDBusFixture super; + +protected: + + static void file_deleter (std::string* s) + { + fprintf(stderr, "remove \"%s\"\n", s->c_str()); + g_remove(s->c_str()); + delete s; + } + + std::shared_ptr<std::string> m_tmpdir; + + void SetUp() + { + super::SetUp(); + + char tmpl[] = {"adb-client-test-XXXXXX"}; + m_tmpdir.reset(new std::string{g_mkdtemp(tmpl)}, file_deleter); + g_message("using tmpdir '%s'", m_tmpdir->c_str()); + } +}; + + +TEST_F(AdbdClientFixture, SocketPlumbing) +{ + struct { + const std::string request; + const std::string expected_pk; + AdbdClient::PKResponse response; + const std::string expected_response; + } tests[] = { + { "PKHelloWorld", "HelloWorld", AdbdClient::PKResponse::ALLOW, "OK" }, + { "PKHelloWorld", "HelloWorld", AdbdClient::PKResponse::DENY, "NO" }, + { "PKFooBar", "FooBar", AdbdClient::PKResponse::ALLOW, "OK" }, + { "PK", "", AdbdClient::PKResponse::DENY, "NO" } + }; + + const auto main_thread = g_thread_self(); + + const auto socket_path = *m_tmpdir + "/test-socket-plumbing"; + g_message("socket_path is %s", socket_path.c_str()); + + for (const auto& test : tests) + { + // start an AdbdClient that listens for PKRequests + std::string pk; + auto adbd_client = std::make_shared<GAdbdClient>(socket_path); + adbd_client->on_pk_request().connect([&pk, main_thread, test](const AdbdClient::PKRequest& req){ + EXPECT_EQ(main_thread, g_thread_self()); + g_message("in on_pk_request with %s", req.public_key.c_str()); + pk = req.public_key; + req.respond(test.response); + }); + + // start a mock AdbdServer with to fire test key requests and wait for a response + auto adbd_server = std::make_shared<GAdbdServer>(socket_path, std::vector<std::string>{test.request}); + wait_for([adbd_server](){return !adbd_server->m_responses.empty();}, 2000); + EXPECT_EQ(test.expected_pk, pk); + ASSERT_EQ(1, adbd_server->m_responses.size()); + EXPECT_EQ(test.expected_response, adbd_server->m_responses.front()); + + // cleanup + adbd_client.reset(); + adbd_server.reset(); + g_unlink(socket_path.c_str()); + } +} diff --git a/tests/test-rotation-lock.cpp b/tests/unit/rotation-lock-test.cpp index 946b1dd..b9630b5 100644 --- a/tests/test-rotation-lock.cpp +++ b/tests/unit/rotation-lock-test.cpp @@ -17,14 +17,14 @@ * Charles Kerr <charles.kerr@canonical.com> */ -#include "gtestdbus-fixture.h" +#include <tests/utils/test-dbus-fixture.h> #include <src/rotation-lock.h> -class RotationLockFixture: public GTestDBusFixture +class RotationLockFixture: public TestDBusFixture { private: - typedef GTestDBusFixture super; + typedef TestDBusFixture super; protected: diff --git a/tests/unit/usb-snap-test.cpp b/tests/unit/usb-snap-test.cpp new file mode 100644 index 0000000..3b778dd --- /dev/null +++ b/tests/unit/usb-snap-test.cpp @@ -0,0 +1,143 @@ +/* + * Copyright 2016 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#include <tests/utils/qt-fixture.h> + +#include <src/dbus-names.h> +#include <src/usb-snap.h> + +#include <libqtdbustest/DBusTestRunner.h> +#include <libqtdbustest/QProcessDBusService.h> +#include <libqtdbusmock/DBusMock.h> + +class UsbSnapFixture: public QtFixture +{ + using super = QtFixture; + +public: + + UsbSnapFixture(): + dbusMock{dbusTestRunner} + { + dbusTestRunner.startServices(); + } + + ~UsbSnapFixture() =default; + +protected: + + void SetUp() override + { + super::SetUp(); + + dbusMock.registerNotificationDaemon(); + dbusTestRunner.startServices(); + } + + OrgFreedesktopDBusMockInterface& notificationsMockInterface() + { + return dbusMock.mockInterface(DBusNames::Notify::NAME, + DBusNames::Notify::PATH, + DBusNames::Notify::INTERFACE, + QDBusConnection::SessionBus); + } + + QtDBusTest::DBusTestRunner dbusTestRunner; + QtDBusMock::DBusMock dbusMock; +}; + +TEST_F(UsbSnapFixture, TestRoundTrip) +{ + struct { + const char* fingerprint; + const char* action_to_invoke; + const AdbdClient::PKResponse expected_response; + } tests[] = { + { "Fingerprint", "allow", AdbdClient::PKResponse::ALLOW }, + { "Fingerprint", "deny", AdbdClient::PKResponse::DENY } + }; + + uint32_t next_id = 1; + for(const auto& test : tests) + { + // Minor wart: we don't have a way of getting the fdo notification id + // from dbusmock so instead we copy its (simple) id generation here + const auto id = next_id++; + + QSignalSpy notificationsSpy( + ¬ificationsMockInterface(), + SIGNAL(MethodCalled(const QString &, const QVariantList &))); + + // start up a UsbSnap to ask about a fingerprint + auto snap = std::make_shared<UsbSnap>(test.fingerprint); + AdbdClient::PKResponse user_response {}; + bool user_response_set = false; + snap->on_user_response().connect([&user_response,&user_response_set](AdbdClient::PKResponse response, bool /*remember*/){ + user_response = response; + user_response_set = true; + }); + + // test that UsbSnap creates a fdo notification + wait_for_signals(notificationsSpy, 1); + { + QVariantList const& call(notificationsSpy.at(0)); + EXPECT_EQ("Notify", call.at(0)); + + QVariantList const& args(call.at(1).toList()); + ASSERT_EQ(8, args.size()); + EXPECT_EQ("", args.at(0)); // app name + EXPECT_EQ(0, args.at(1)); // replaces-id + EXPECT_EQ("computer-symbolic", args.at(2)); // icon name + EXPECT_EQ("Allow USB Debugging?", args.at(3)); // summary + EXPECT_EQ(QString::fromUtf8("The computer's RSA key fingerprint is: ") + test.fingerprint, args.at(4)); // body + EXPECT_EQ(QStringList({"allow", "Allow", "deny", "Don't Allow"}), args.at(5)); // actions + EXPECT_EQ(-1, args.at(7)); + + QVariantMap hints; + ASSERT_TRUE(qDBusArgumentToMap(args.at(6), hints)); + ASSERT_EQ(3, hints.size()); + ASSERT_TRUE(hints.contains("x-canonical-private-affirmative-tint")); + ASSERT_TRUE(hints.contains("x-canonical-non-shaped-icon")); + ASSERT_TRUE(hints.contains("x-canonical-snap-decisions")); + } + notificationsSpy.clear(); + + // fake a user interaction with the fdo notification + notificationsMockInterface().EmitSignal( + DBusNames::Notify::INTERFACE, + DBusNames::Notify::ActionInvoked::NAME, + "us", + QVariantList() << id << test.action_to_invoke); + + // test that UsbSnap emits on_user_response() as a result + wait_for([&user_response_set](){return user_response_set;}); + EXPECT_TRUE(user_response_set); + ASSERT_EQ(test.expected_response, user_response); + + // confirm that the snap dtor cleans up the notification + snap.reset(); + wait_for_signals(notificationsSpy, 1); + { + QVariantList const& call(notificationsSpy.at(0)); + EXPECT_EQ("CloseNotification", call.at(0)); + QVariantList const& args(call.at(1).toList()); + EXPECT_EQ(id, args.at(0)); + } + } +} diff --git a/tests/utils/CMakeLists.txt b/tests/utils/CMakeLists.txt new file mode 100644 index 0000000..e458c82 --- /dev/null +++ b/tests/utils/CMakeLists.txt @@ -0,0 +1,17 @@ +include_directories( + ${CMAKE_CURRENT_BINARY_DIR} + ${CMAKE_CURRENT_SOURCE_DIR} +) + +add_library( + test-utils + STATIC + qmain.cpp +) + +qt5_use_modules( + test-utils + Core + DBus +) + diff --git a/tests/utils/adbd-server.h b/tests/utils/adbd-server.h new file mode 100644 index 0000000..b574622 --- /dev/null +++ b/tests/utils/adbd-server.h @@ -0,0 +1,150 @@ +/* + * Copyright 2016 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#include <gio/gio.h> +#include <gio/gunixsocketaddress.h> + +#include <string> +#include <thread> +#include <vector> + + +/** + * A Mock ADBD server. + * + * Binds to a local domain socket, sends public key requests across it, + * and reads back the client's responses. + */ +class GAdbdServer +{ +public: + + GAdbdServer(const std::string& socket_path, + const std::vector<std::string>& requests): + m_requests{requests}, + m_socket_path{socket_path}, + m_cancellable{g_cancellable_new()}, + m_worker_thread{&GAdbdServer::worker_func, this} + { + } + + ~GAdbdServer() + { + // tell the worker thread to stop whatever it's doing and exit. + g_cancellable_cancel(m_cancellable); + m_worker_thread.join(); + g_clear_object(&m_cancellable); + } + + const std::vector<std::string> m_requests; + std::vector<std::string> m_responses; + +private: + + void worker_func() // runs in worker thread + { + auto server_socket = create_server_socket(m_socket_path); + auto requests = m_requests; + + GError* error {}; + g_socket_listen (server_socket, &error); + g_assert_no_error (error); + + while (!g_cancellable_is_cancelled(m_cancellable) && !requests.empty()) + { + // wait for a client connection + g_message("GAdbdServer::Impl::worker_func() calling g_socket_accept()"); + auto client_socket = g_socket_accept(server_socket, m_cancellable, &error); + if (error != nullptr) { + if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_message("GAdbdServer: Error accepting socket connection: %s", error->message); + g_clear_error(&error); + break; + } + + // pop the next request off the stack + auto request = requests.front(); + + // send the request + g_message("GAdbdServer::Impl::worker_func() sending req [%s]", request.c_str()); + g_socket_send(client_socket, + request.c_str(), + request.size(), + m_cancellable, + &error); + if (error != nullptr) { + if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_message("GAdbdServer: Error sending request: %s", error->message); + g_clear_error(&error); + g_clear_object(&client_socket); + break; + } + + // read the response + g_message("GAdbdServer::Impl::worker_func() reading response"); + char buf[4096]; + const auto n_bytes = g_socket_receive(client_socket, + buf, + sizeof(buf), + m_cancellable, + &error); + if (error != nullptr) { + if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_message("GAdbdServer: Error reading response: %s", error->message); + g_clear_error(&error); + g_clear_object(&client_socket); + continue; + } + const std::string response(buf, std::string::size_type(n_bytes)); + g_message("server read %d bytes, got response: '%s'", int(n_bytes), response.c_str()); + if (!response.empty()) { + m_responses.push_back(response); + requests.erase(requests.begin()); + } + + // cleanup + g_clear_object(&client_socket); + } + + g_clear_object(&server_socket); + } + + // bind to a local domain socket + static GSocket* create_server_socket(const std::string& socket_path) + { + GError* error {}; + auto socket = g_socket_new(G_SOCKET_FAMILY_UNIX, + G_SOCKET_TYPE_STREAM, + G_SOCKET_PROTOCOL_DEFAULT, + &error); + g_assert_no_error(error); + auto address = g_unix_socket_address_new (socket_path.c_str()); + g_socket_bind (socket, address, false, &error); + g_assert_no_error (error); + g_clear_object (&address); + + return socket; + } + + const std::string m_socket_path; + GCancellable* m_cancellable = nullptr; + std::thread m_worker_thread; +}; + + diff --git a/tests/utils/dbus-types.h b/tests/utils/dbus-types.h new file mode 100644 index 0000000..3b3a02d --- /dev/null +++ b/tests/utils/dbus-types.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2013-2016 Canonical, Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>. + * + * Author: Pete Woods <pete.woods@canonical.com> + */ + +#pragma once + +#include <QDBusMetaType> +#include <QtCore> +#include <QString> +#include <QVariantMap> + +typedef QMap<QString, QVariantMap> QVariantDictMap; +Q_DECLARE_METATYPE(QVariantDictMap) + +typedef QMap<QString, QString> QStringMap; +Q_DECLARE_METATYPE(QStringMap) + +namespace DBusTypes +{ + inline void registerMetaTypes() + { + qRegisterMetaType<QVariantDictMap>("QVariantDictMap"); + qRegisterMetaType<QStringMap>("QStringMap"); + + qDBusRegisterMetaType<QVariantDictMap>(); + qDBusRegisterMetaType<QStringMap>(); + } +} diff --git a/tests/utils/glib-fixture.h b/tests/utils/glib-fixture.h new file mode 100644 index 0000000..ccdeccd --- /dev/null +++ b/tests/utils/glib-fixture.h @@ -0,0 +1,203 @@ +/* + * Copyright 2013 Canonical Ltd. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <functional> // std::function +#include <map> +#include <memory> // std::shared_ptr + +#include <glib.h> +#include <glib/gstdio.h> +#include <gio/gio.h> + +#include <gtest/gtest.h> + +#include <locale.h> // setlocale() + +class GlibFixture : public ::testing::Test +{ + public: + + virtual ~GlibFixture() =default; + + protected: + + virtual void SetUp() override + { + setlocale(LC_ALL, "C.UTF-8"); + + loop = g_main_loop_new(nullptr, false); + +#ifdef SCHEMA_DIR + // only use local, temporary settings + g_assert(g_setenv("GSETTINGS_SCHEMA_DIR", SCHEMA_DIR, true)); + g_assert(g_setenv("GSETTINGS_BACKEND", "memory", true)); + g_debug("SCHEMA_DIR is %s", SCHEMA_DIR); +#endif + + // fail on unexpected messages from this domain + g_log_set_fatal_mask(G_LOG_DOMAIN, G_LOG_LEVEL_WARNING); + + g_unsetenv("DISPLAY"); + + } + + virtual void TearDown() override + { + g_test_assert_expected_messages (); + + g_clear_pointer(&loop, g_main_loop_unref); + } + + void expectLogMessage (const gchar *domain, GLogLevelFlags level, const gchar *pattern) + { + g_test_expect_message (domain, level, pattern); + } + + private: + + static gboolean + wait_for_signal__timeout(gpointer name) + { + g_error("%s: timed out waiting for signal '%s'", G_STRLOC, static_cast<const char*>(name)); + return G_SOURCE_REMOVE; + } + + static gboolean + wait_msec__timeout(gpointer loop) + { + g_main_loop_quit(static_cast<GMainLoop*>(loop)); + return G_SOURCE_CONTINUE; + } + + protected: + + /* convenience func to loop while waiting for a GObject's signal */ + void wait_for_signal(gpointer o, const gchar * signal, guint timeout_seconds=5) + { + // wait for the signal or for timeout, whichever comes first + const auto handler_id = g_signal_connect_swapped(o, signal, + G_CALLBACK(g_main_loop_quit), + loop); + const auto timeout_id = g_timeout_add_seconds(timeout_seconds, + wait_for_signal__timeout, + loop); + g_main_loop_run(loop); + g_source_remove(timeout_id); + g_signal_handler_disconnect(o, handler_id); + } + + /* convenience func to loop for N msec */ + void wait_msec(guint msec=50) + { + const auto id = g_timeout_add(msec, wait_msec__timeout, loop); + g_main_loop_run(loop); + g_source_remove(id); + } + + bool wait_for(std::function<bool()> test_function, guint timeout_msec=1000) + { + auto timer = std::shared_ptr<GTimer>(g_timer_new(), [](GTimer* t){g_timer_destroy(t);}); + const auto timeout_sec = timeout_msec / 1000.0; + for (;;) { + if (test_function()) + return true; + //g_message("%f ... %f", g_timer_elapsed(timer.get(), nullptr), timeout_sec); + if (g_timer_elapsed(timer.get(), nullptr) >= timeout_sec) + return false; + wait_msec(); + } + } + + bool wait_for_name_owned(GDBusConnection* connection, + const gchar* name, + guint timeout_msec=1000, + GBusNameWatcherFlags flags=G_BUS_NAME_WATCHER_FLAGS_AUTO_START) + { + struct Data { + GMainLoop* loop = nullptr; + bool owned = false; + }; + Data data; + + auto on_name_appeared = [](GDBusConnection* /*connection*/, + const gchar* /*name_*/, + const gchar* name_owner, + gpointer gdata) + { + if (name_owner == nullptr) + return; + auto tmp = static_cast<Data*>(gdata); + tmp->owned = true; + g_main_loop_quit(tmp->loop); + }; + + const auto timeout_id = g_timeout_add(timeout_msec, wait_msec__timeout, loop); + data.loop = loop; + const auto watch_id = g_bus_watch_name_on_connection(connection, + name, + flags, + on_name_appeared, + nullptr, /* name_vanished */ + &data, + nullptr); /* user_data_free_func */ + g_main_loop_run(loop); + + g_bus_unwatch_name(watch_id); + g_source_remove(timeout_id); + + return data.owned; + } + + void EXPECT_NAME_OWNED_EVENTUALLY(GDBusConnection* connection, + const gchar* name, + guint timeout_msec=1000, + GBusNameWatcherFlags flags=G_BUS_NAME_WATCHER_FLAGS_AUTO_START) + { + EXPECT_TRUE(wait_for_name_owned(connection, name, timeout_msec, flags)) << "name: " << name; + } + + void EXPECT_NAME_NOT_OWNED_EVENTUALLY(GDBusConnection* connection, + const gchar* name, + guint timeout_msec=1000, + GBusNameWatcherFlags flags=G_BUS_NAME_WATCHER_FLAGS_AUTO_START) + { + EXPECT_FALSE(wait_for_name_owned(connection, name, timeout_msec, flags)) << "name: " << name; + } + + void ASSERT_NAME_OWNED_EVENTUALLY(GDBusConnection* connection, + const gchar* name, + guint timeout_msec=1000, + GBusNameWatcherFlags flags=G_BUS_NAME_WATCHER_FLAGS_AUTO_START) + { + ASSERT_TRUE(wait_for_name_owned(connection, name, timeout_msec, flags)) << "name: " << name; + } + + void ASSERT_NAME_NOT_OWNED_EVENTUALLY(GDBusConnection* connection, + const gchar* name, + guint timeout_msec=1000, + GBusNameWatcherFlags flags=G_BUS_NAME_WATCHER_FLAGS_AUTO_START) + { + ASSERT_FALSE(wait_for_name_owned(connection, name, timeout_msec, flags)) << "name: " << name; + } + + GMainLoop * loop; +}; + diff --git a/tests/utils/gtest-qt-print-helpers.h b/tests/utils/gtest-qt-print-helpers.h new file mode 100644 index 0000000..7a0897e --- /dev/null +++ b/tests/utils/gtest-qt-print-helpers.h @@ -0,0 +1,45 @@ + +#pragma once + +#include <QDBusObjectPath> +#include <QString> +#include <QStringList> +#include <QVariant> + +inline QString qVariantToString(const QVariant& variant) { + QString output; + QDebug dbg(&output); + dbg << variant; + return output; +} + +inline void PrintTo(const QVariant& variant, std::ostream* os) { + QString output; + QDebug dbg(&output); + dbg << variant; + + *os << "QVariant(" << output.toStdString() << ")"; +} + +inline void PrintTo(const QString& s, std::ostream* os) { + *os << "\"" << s.toStdString() << "\""; +} + +inline void PrintTo(const QStringList& list, std::ostream* os) { + QString output; + QDebug dbg(&output); + dbg << list; + + *os << "QStringList(" << output.toStdString() << ")"; +} + +inline void PrintTo(const QList<QDBusObjectPath>& list, std::ostream* os) { + QString output; + for (const auto& path: list) + { + output.append("\"" + path.path() + "\","); + } + + *os << "QList<QDBusObjectPath>(" << output.toStdString() << ")"; +} + diff --git a/tests/utils/mock-greeter.h b/tests/utils/mock-greeter.h new file mode 100644 index 0000000..5ac85a0 --- /dev/null +++ b/tests/utils/mock-greeter.h @@ -0,0 +1,32 @@ +/* + * Copyright 2016 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#pragma once + +#include <src/greeter.h> + +class MockGreeter: public Greeter +{ +public: + MockGreeter() =default; + virtual ~MockGreeter() =default; + core::Property<bool>& is_active() override {return m_is_active;} + core::Property<bool> m_is_active {false}; +}; + diff --git a/tests/utils/mock-usb-monitor.h b/tests/utils/mock-usb-monitor.h new file mode 100644 index 0000000..92b89db --- /dev/null +++ b/tests/utils/mock-usb-monitor.h @@ -0,0 +1,32 @@ +/* + * Copyright 2016 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#pragma once + +#include <src/usb-monitor.h> + +class MockUsbMonitor: public UsbMonitor +{ +public: + MockUsbMonitor() =default; + virtual ~MockUsbMonitor() =default; + core::Signal<const std::string&>& on_usb_disconnected() override {return m_on_usb_disconnected;} + core::Signal<const std::string&> m_on_usb_disconnected; +}; + diff --git a/tests/utils/qmain.cpp b/tests/utils/qmain.cpp new file mode 100644 index 0000000..72a49b1 --- /dev/null +++ b/tests/utils/qmain.cpp @@ -0,0 +1,60 @@ +/* + * Copyright © 2014 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 3, + * as published by the Free Software Foundation. + * + * 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/>. + * + * Authors: + * Pete Woods <pete.woods@canonical.com> + */ + +//#include <config.h> + +#include <QCoreApplication> +#include <QTimer> +#include <gtest/gtest.h> + +#include <libqtdbusmock/DBusMock.h> + +using namespace QtDBusMock; + +class Runner: public QObject +{ + Q_OBJECT +public Q_SLOTS: + void run() + { + QCoreApplication::exit(RUN_ALL_TESTS()); + } +}; + +int main(int argc, char **argv) +{ + qputenv("LANG", "C.UTF-8"); + unsetenv("LC_ALL"); + + // boilerplate i18n + setlocale(LC_ALL, ""); + bindtextdomain(GETTEXT_PACKAGE, GNOMELOCALEDIR); + textdomain(GETTEXT_PACKAGE); + + QCoreApplication application(argc, argv); + DBusMock::registerMetaTypes(); + ::testing::InitGoogleTest(&argc, argv); + + Runner runner; + QTimer::singleShot(0, &runner, SLOT(run())); + + return application.exec(); +} + +#include "qmain.moc" diff --git a/tests/utils/qt-fixture.h b/tests/utils/qt-fixture.h new file mode 100644 index 0000000..95b9b14 --- /dev/null +++ b/tests/utils/qt-fixture.h @@ -0,0 +1,74 @@ +/* + * Copyright 2016 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#pragma once + +#define QT_NO_KEYWORDS + +#include <tests/utils/dbus-types.h> +#include <tests/utils/glib-fixture.h> +#include <tests/utils/gtest-qt-print-helpers.h> + +#include <gtest/gtest.h> + +#include <QDBusArgument> +#include <QVariant> +#include <QSignalSpy> + +#define wait_for_signals(signalSpy,signalsExpected) \ +{ \ + while (signalSpy.size() < signalsExpected) \ + { \ + ASSERT_TRUE(signalSpy.wait()); \ + } \ + \ + ASSERT_EQ(signalsExpected, signalSpy.size()); \ +} + +class QtFixture: public GlibFixture +{ + using super = GlibFixture; + +public: + + QtFixture() + { + DBusTypes::registerMetaTypes(); + } + + ~QtFixture() =default; + +protected: + + bool qDBusArgumentToMap(QVariant const& variant, QVariantMap& map) + { + if (variant.canConvert<QDBusArgument>()) + { + QDBusArgument value(variant.value<QDBusArgument>()); + if (value.currentType() == QDBusArgument::MapType) + { + value >> map; + return true; + } + } + + return false; + } +}; + diff --git a/tests/gtestdbus-fixture.h b/tests/utils/test-dbus-fixture.h index c592033..3947e58 100644 --- a/tests/gtestdbus-fixture.h +++ b/tests/utils/test-dbus-fixture.h @@ -17,8 +17,7 @@ * Charles Kerr <charles.kerr@canonical.com> */ -#ifndef INDICATOR_TESTS_GTESTDBUS_FIXTURE_H -#define INDICATOR_TESTS_GTESTDBUS_FIXTURE_H +#pragma once #include "glib-fixture.h" @@ -26,14 +25,14 @@ **** ***/ -class GTestDBusFixture: public GlibFixture +class TestDBusFixture: public GlibFixture { public: - GTestDBusFixture() =default; - virtual ~GTestDBusFixture() =default; + TestDBusFixture() =default; + virtual ~TestDBusFixture() =default; - explicit GTestDBusFixture(const std::vector<std::string>& service_dirs_in): service_dirs(service_dirs_in) {} + explicit TestDBusFixture(const std::vector<std::string>& service_dirs_in): service_dirs(service_dirs_in) {} private: @@ -42,10 +41,10 @@ class GTestDBusFixture: public GlibFixture static void on_bus_opened (GObject* /*object*/, GAsyncResult * res, gpointer gself) { - auto self = static_cast<GTestDBusFixture*>(gself); + auto self = static_cast<TestDBusFixture*>(gself); GError * err = 0; - self->bus = g_bus_get_finish (res, &err); + self->system_bus = g_bus_get_finish (res, &err); g_assert_no_error (err); g_main_loop_quit (self->loop); @@ -54,10 +53,10 @@ class GTestDBusFixture: public GlibFixture static void on_bus_closed (GObject* /*object*/, GAsyncResult * res, gpointer gself) { - auto self = static_cast<GTestDBusFixture*>(gself); + auto self = static_cast<TestDBusFixture*>(gself); GError * err = 0; - g_dbus_connection_close_finish (self->bus, res, &err); + g_dbus_connection_close_finish (self->system_bus, res, &err); g_assert_no_error (err); g_main_loop_quit (self->loop); @@ -66,10 +65,10 @@ class GTestDBusFixture: public GlibFixture protected: GTestDBus * test_dbus = nullptr; - GDBusConnection * bus = nullptr; + GDBusConnection * system_bus = nullptr; const std::vector<std::string> service_dirs; - virtual void SetUp () + virtual void SetUp() override { super::SetUp (); @@ -88,14 +87,14 @@ class GTestDBusFixture: public GlibFixture g_main_loop_run (loop); } - virtual void TearDown () + virtual void TearDown() override { wait_msec(); // close the system bus - g_dbus_connection_close(bus, nullptr, on_bus_closed, this); + g_dbus_connection_close(system_bus, nullptr, on_bus_closed, this); g_main_loop_run(loop); - g_clear_object(&bus); + g_clear_object(&system_bus); // tear down the test dbus g_test_dbus_down(test_dbus); @@ -105,4 +104,3 @@ class GTestDBusFixture: public GlibFixture } }; -#endif /* INDICATOR_TESTS_GTESTDBUS_FIXTURE_H */ |