File src.obscpio of Package AppImageLauncher
07070100000000000081a400000000000000000000000168cf6940000006e3000000000000000000000000000000000000001300000000src/CMakeLists.txt# in version 3.6 IMPORTED_TARGET has been added to pkg_*_modules
cmake_minimum_required(VERSION 3.6)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
# Find includes in corresponding build directories
set(CMAKE_INCLUDE_CURRENT_DIR ON)
# Instruct CMake to run moc automatically when needed
set(CMAKE_AUTOMOC ON)
# Create code from a list of Qt designer ui files
set(CMAKE_AUTOUIC ON)
# Compile resource files
set(CMAKE_AUTORCC ON)
find_package(Qt5 REQUIRED COMPONENTS Core Widgets DBus Quick QuickWidgets Qml)
find_package(PkgConfig REQUIRED)
pkg_check_modules(glib REQUIRED glib-2.0>=2.40 IMPORTED_TARGET)
find_package(INotify REQUIRED)
# disable Qt debug messages except for debug builds
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
message(STATUS "CMake build type is ${CMAKE_BUILD_TYPE}, hence disabling debug messages in AppImageLauncher")
add_definitions(-DQT_NO_DEBUG_OUTPUT)
add_definitions(-DQT_NO_INFO_OUTPUT)
add_definitions(-DQT_NO_WARNING_OUTPUT)
endif()
# this shall propagate to all targets built in the subdirs
if(ENABLE_UPDATE_HELPER)
add_definitions(-DENABLE_UPDATE_HELPER)
endif()
add_compile_options(-Wall -Wpedantic)
if(BUILD_LITE)
add_definitions(-DBUILD_LITE)
endif()
# utility libraries
add_subdirectory(fswatcher)
add_subdirectory(i18n)
add_subdirectory(trashbin)
add_subdirectory(shared)
# appimagelauncherd
add_subdirectory(daemon)
# main application and helper tools
add_subdirectory(ui)
# no need to include bypass launcher in lite builds
if(NOT BUILD_LITE)
add_subdirectory(binfmt-bypass)
endif()
# CLI helper allowing other tools to utilize AppImageLauncher's code for e.g., integrating AppImages
add_subdirectory(cli)
07070100000001000081a400000000000000000000000168cf694000001ed9000000000000000000000000000000000000002100000000src/binfmt-bypass/CMakeLists.txt# needed for LINK_OPTIONS/target_link_options
cmake_minimum_required(VERSION 3.13)
project(appimage-binfmt-bypass)
# configure names globally
set(bypass_bin binfmt-bypass)
set(binfmt_interpreter binfmt-interpreter)
set(bypass_lib lib${bypass_bin})
set(preload_lib ${bypass_bin}-preload)
# we're not using "i386" or "armhf" but instead the more generic "32bit" suffix, making things a little easier in the
# code
set(preload_lib_32bit ${preload_lib}_32bit)
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
string(TOLOWER ${CMAKE_C_COMPILER_ID} lower_cmake_c_compiler_id)
# on 64-bit systems such as x86_64 or aarch64/arm64v8, we need to provide a 32-bit preload library to allow the users
# to execute compatible 32-bit binaries, as the regular preload library cannot be preloaded due to the architecture
# being incompatible
# unfortunately, compiling on ARM is significantly harder than just adding a compiler flag, as gcc-multilib is missing
# it's likely easier with clang, but that remains to be verified
if(CMAKE_SYSTEM_PROCESSOR MATCHES x86_64)
message(STATUS "64-bit target detected (${CMAKE_SYSTEM_PROCESSOR}), building 32-bit preload library as well")
set(build_32bit_preload_library true)
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES arm64 OR CMAKE_SYSTEM_PROCESSOR MATCHES aarch64)
string(TOLOWER ${CMAKE_C_COMPILER_ID} lower_cmake_c_compiler_id)
if(lower_cmake_c_compiler_id MATCHES clang)
# clang-3.8, the default clang on xenial, doesn't like the templates in elf.cpp with the varying return types
if(NOT ${CMAKE_CXX_COMPILER_VERSION} VERSION_GREATER 3.9.0)
message(FATAL_ERROR "clang too old, please upgrade to, e.g., clang-8")
endif()
message(STATUS "64-bit target detected (${CMAKE_SYSTEM_PROCESSOR}), building 32-bit preload library as well")
set(build_32bit_preload_library true)
else()
message(WARNING "64-bit target detected (${CMAKE_SYSTEM_PROCESSOR}), but compiling with ${lower_cmake_c_compiler_id}, cannot build 32-bit preload library")
endif()
endif()
function(make_preload_lib_target target_name)
# library to be preloaded when launching the patched runtime binary
# we need to build with -fPIC, otherwise we can't use it with $LD_PRELOAD
add_library(${target_name} SHARED preload.c logging.h)
target_link_libraries(${target_name} PRIVATE dl)
target_compile_options(${target_name}
PRIVATE -fPIC
PRIVATE -DCOMPONENT_NAME="preload"
# hide all symbols by default
PRIVATE -fvisibility=hidden
)
# compatibility with CMake < 3.13
set_target_properties(${target_name} PROPERTIES LINK_OPTIONS
# hide all symbols by default
-fvisibility=hidden
)
# a bit of a hack, but it seems to make binfmt bypass work in really old Docker images (e.g., CentOS <= 7)
add_custom_command(
TARGET ${target_name}
POST_BUILD
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/fix-preload-library.sh $<TARGET_FILE_NAME:${target_name}>
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
VERBATIM
)
endfunction()
make_preload_lib_target(${preload_lib})
if(build_32bit_preload_library)
make_preload_lib_target(${preload_lib_32bit})
if(CMAKE_SYSTEM_PROCESSOR MATCHES x86_64)
if(lower_cmake_c_compiler_id MATCHES clang)
target_compile_options(${preload_lib_32bit} PRIVATE "--target=i386-linux-gnu")
target_link_options(${preload_lib_32bit} PRIVATE "--target=i386-linux-gnu")
else()
# GCC style flags
target_compile_options(${preload_lib_32bit} PRIVATE "-m32")
target_link_options(${preload_lib_32bit} PRIVATE "-m32")
endif()
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES arm64 OR CMAKE_SYSTEM_PROCESSOR MATCHES aarch64)
target_compile_options(${preload_lib_32bit} PRIVATE "--target=arm-linux-gnueabihf")
target_link_options(${preload_lib_32bit} PRIVATE "--target=arm-linux-gnueabihf")
else()
message(FATAL_ERROR "unknown processor architecture: ${CMAKE_SYSTEM_PROCESSOR}")
endif()
endif()
# we need to make the library below fully self-contained so that it works in other cgroups (e.g., in Docker containers)
# out of the box
# this allows us to check whether the path is available, and otherwise create a temporary file and use that then
# this is a workaround to existing issues using AppImages in Docker with AppImageLauncher installed on the host system
check_program(NAME xxd)
function(generate_preload_lib_header target_name)
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${target_name}.h
COMMAND xxd -i $<TARGET_FILE_NAME:${target_name}> ${target_name}.h
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
DEPENDS ${target_name}
VERBATIM
)
endfunction()
generate_preload_lib_header(${preload_lib})
# same story for 32-bit lib
if (build_32bit_preload_library)
generate_preload_lib_header(${preload_lib_32bit})
endif()
# the lib provides an algorithm to extract the runtime, patch it and launch it, preloading our preload lib to make the
# AppImage think it is launched normally
# static linking is preferred, since we do not want to deal with an installed .so file, rpaths etc.
add_library(${bypass_lib} STATIC lib.cpp elf.cpp logging.h elf.h ${CMAKE_CURRENT_BINARY_DIR}/${preload_lib}.h)
target_link_libraries(${bypass_lib} PUBLIC dl)
# we need to include the preload lib headers (see below) from the binary dir
target_include_directories(${bypass_lib} PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_compile_options(${bypass_lib}
PRIVATE -DPRELOAD_LIB_NAME="$<TARGET_FILE_NAME:${preload_lib}>"
PUBLIC -D_GNU_SOURCE
# obviously needs to be private, otherwise the value leaks into users of this lib
PRIVATE -DCOMPONENT_NAME="lib"
)
if(build_32bit_preload_library)
target_compile_options(${bypass_lib}
PRIVATE -DPRELOAD_LIB_NAME_32BIT="$<TARGET_FILE_NAME:${preload_lib_32bit}>"
)
target_sources(${bypass_lib} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/${preload_lib_32bit}.h)
endif()
include(CheckCSourceCompiles)
message(STATUS "Checking whether memfd_create(...) is available")
check_c_source_compiles("
#define _GNU_SOURCE
#include <sys/mman.h>
int main(int argc, char** argv) {
memfd_create(\"test\", 0);
}
"
HAVE_MEMFD_CREATE
)
if(HAVE_MEMFD_CREATE)
target_compile_options(${bypass_lib} PUBLIC -DHAVE_MEMFD_CREATE)
else()
message(WARNING "memfd_create not available, falling back to shm_open")
target_link_libraries(${bypass_lib} PRIVATE rt)
endif()
add_executable(${bypass_bin} bypass_main.cpp)
target_link_libraries(${bypass_bin} ${bypass_lib})
target_compile_options(${bypass_bin}
PRIVATE -DCOMPONENT_NAME="bin"
)
add_executable(${binfmt_interpreter} interpreter_main.cpp)
target_link_libraries(${binfmt_interpreter} ${bypass_lib})
target_compile_options(${binfmt_interpreter}
PRIVATE -DCOMPONENT_NAME="interpreter"
PRIVATE -DAPPIMAGELAUNCHER_PATH="${CMAKE_INSTALL_PREFIX}/${_bindir}/$<TARGET_FILE_NAME:AppImageLauncher>"
)
# static linking makes the F (fix binary) mode more reliable, as the binary as well as all its resources are completely
# preloaded by binfmt-misc, avoiding runtime dependencies on libc, libstdc++ etc.
target_link_options(${binfmt_interpreter}
PRIVATE -static -static-libgcc -static-libstdc++
)
# no need to set rpath on bypass binary, it doesn't depend on any private libraries
# the preload lib is used via its absolute path anyway
install(
TARGETS ${bypass_bin} ${preload_lib} ${binfmt_interpreter}
RUNTIME DESTINATION ${_private_libdir} COMPONENT APPIMAGELAUNCHER
LIBRARY DESTINATION ${_private_libdir} COMPONENT APPIMAGELAUNCHER
)
if(build_32bit_preload_library)
install(
TARGETS ${preload_lib}
LIBRARY DESTINATION ${_private_libdir}
)
endif()
07070100000002000081a400000000000000000000000168cf69400000016e000000000000000000000000000000000000002200000000src/binfmt-bypass/bypass_main.cpp// system headers
#include <vector>
// own headers
#include "logging.h"
#include "lib.h"
int main(int argc, char** argv) {
if (argc <= 1) {
log_message("Usage: %s <AppImage file> [args...]\n", argv[0]);
return EXIT_CODE_FAILURE;
}
std::vector<char*> args(&argv[2], &argv[argc]);
return bypassBinfmtAndRunAppImage(argv[1], args);
}
07070100000003000081a400000000000000000000000168cf6940000018c1000000000000000000000000000000000000001a00000000src/binfmt-bypass/elf.cpp// system headers
#include <cstdio>
#include <cstdlib>
#include <cstdint>
#include <type_traits>
#include <linux/elf.h>
#include <byteswap.h>
#include <fstream>
#include <iostream>
#include <stdexcept>
// own headers
#include "elf.h"
#include "logging.h"
#if __BYTE_ORDER == __LITTLE_ENDIAN
#define NATIVE_BYTE_ORDER ELFDATA2LSB
#elif __BYTE_ORDER == __BIG_ENDIAN
#define NATIVE_BYTE_ORDER ELFDATA2MSB
#else
#error "Unknown machine endian"
#endif
template<typename T>
T bswap(T val) = delete;
template<>
uint16_t bswap(uint16_t val) {
return bswap_16(val);
}
template<>
uint32_t bswap(uint32_t val) {
return bswap_32(val);
}
template<>
unsigned long long bswap(unsigned long long val) {
return bswap_64(val);
}
template<typename EhdrT, typename ValT>
void swap_data_if_necessary(const EhdrT& ehdr, ValT& val) {
static_assert(std::is_same<Elf64_Ehdr, EhdrT>::value || std::is_same<Elf32_Ehdr, EhdrT>::value,
"must be Elf{32,64}_Ehdr");
if (ehdr.e_ident[EI_DATA] != NATIVE_BYTE_ORDER) {
val = bswap(val);
}
}
template<typename EhdrT, typename ShdrT>
off_t get_elf_size(std::ifstream& ifs)
{
static_assert(std::is_same<Elf64_Ehdr, EhdrT>::value || std::is_same<Elf32_Ehdr, EhdrT>::value,
"must be Elf{32,64}_Ehdr");
static_assert(std::is_same<Elf64_Shdr, ShdrT>::value || std::is_same<Elf32_Shdr, ShdrT>::value,
"must be Elf{32,64}_Shdr");
EhdrT elf_header{};
ifs.seekg(0, std::ifstream::beg);
ifs.read(reinterpret_cast<char*>(&elf_header), sizeof(elf_header));
if (!ifs) {
log_error("failed to read ELF header\n");
return -1;
}
swap_data_if_necessary(elf_header, elf_header.e_shoff);
swap_data_if_necessary(elf_header, elf_header.e_shentsize);
swap_data_if_necessary(elf_header, elf_header.e_shnum);
swap_data_if_necessary(elf_header, elf_header.e_shnum);
off_t last_shdr_offset = elf_header.e_shoff + (elf_header.e_shentsize * (elf_header.e_shnum - 1));
ShdrT section_header{};
ifs.seekg(last_shdr_offset, std::ifstream::beg);
ifs.read(reinterpret_cast<char*>(§ion_header), sizeof(elf_header));
if (!ifs) {
log_error("failed to read ELF section header\n");
return -1;
}
swap_data_if_necessary(elf_header, section_header.sh_offset);
swap_data_if_necessary(elf_header, section_header.sh_size);
/* ELF ends either with the table of section headers (SHT) or with a section. */
off_t sht_end = elf_header.e_shoff + (elf_header.e_shentsize * elf_header.e_shnum);
off_t last_section_end = section_header.sh_offset + section_header.sh_size;
return sht_end > last_section_end ? sht_end : last_section_end;
}
template<typename EhdrT, typename ShdrT>
ssize_t get_pt_dynamic_offset(std::ifstream& ifs)
{
static_assert(std::is_same<Elf64_Ehdr, EhdrT>::value || std::is_same<Elf32_Ehdr, EhdrT>::value,
"must be Elf{32,64}_Ehdr");
static_assert(std::is_same<Elf64_Shdr, ShdrT>::value || std::is_same<Elf32_Shdr, ShdrT>::value,
"must be Elf{32,64}_Shdr");
EhdrT elf_header{};
ifs.seekg(0, std::ifstream::beg);
ifs.read(reinterpret_cast<char*>(&elf_header), sizeof(elf_header));
if (!ifs) {
log_error("failed to read ELF header\n");
return -1;
}
swap_data_if_necessary(elf_header, elf_header.e_shoff);
swap_data_if_necessary(elf_header, elf_header.e_shentsize);
swap_data_if_necessary(elf_header, elf_header.e_shnum);
swap_data_if_necessary(elf_header, elf_header.e_shnum);
off_t last_shdr_offset = elf_header.e_shoff + (elf_header.e_shentsize * (elf_header.e_shnum - 1));
ShdrT section_header{};
ifs.seekg(last_shdr_offset, std::ifstream::beg);
ifs.read(reinterpret_cast<char*>(§ion_header), sizeof(elf_header));
if (!ifs) {
log_error("failed to read ELF section header\n");
return -1;
}
swap_data_if_necessary(elf_header, section_header.sh_offset);
swap_data_if_necessary(elf_header, section_header.sh_size);
/* ELF ends either with the table of section headers (SHT) or with a section. */
off_t sht_end = elf_header.e_shoff + (elf_header.e_shentsize * elf_header.e_shnum);
off_t last_section_end = section_header.sh_offset + section_header.sh_size;
return sht_end > last_section_end ? sht_end : last_section_end;
}
bool is_32bit_elf(std::ifstream& ifs) {
if (!ifs) {
log_error("failed to read e_ident from ELF file\n");
return -1;
}
// for the beginning, we just need to read e_ident to determine ELF class (i.e., either 32-bit or 64-bit)
// that way, we can decide which way to go
// the easiest way is to just use the ELF API
Elf64_Ehdr ehdr;
ifs.read(reinterpret_cast<char*>(&ehdr.e_ident), EI_NIDENT);
switch (ehdr.e_ident[EI_CLASS]) {
case ELFCLASS32: {
return true;
}
case ELFCLASS64: {
return false;
}
}
throw std::logic_error{"ELF binary is neither 32-bit nor 64-bit"};
}
bool is_32bit_elf(const std::string& filename) {
std::ifstream ifs(filename);
if (!ifs) {
log_error("could not open file: %s\n", filename.c_str());
return -1;
}
return is_32bit_elf(ifs);
}
bool is_statically_linked_elf(std::ifstream& ifs) {
if (!ifs) {
log_error("failed to read e_ident from ELF file\n");
return -1;
}
ssize_t pt_dynamic_offset;
if (is_32bit_elf(ifs)) {
pt_dynamic_offset = get_pt_dynamic_offset<Elf32_Ehdr, Elf32_Shdr>(ifs);
} else {
pt_dynamic_offset = get_pt_dynamic_offset<Elf64_Ehdr, Elf64_Shdr>(ifs);
}
return pt_dynamic_offset != -1;
}
bool is_statically_linked_elf(const std::string& filename) {
std::ifstream ifs(filename);
if (!ifs) {
log_error("could not open file: %s\n", filename.c_str());
return -1;
}
return is_statically_linked_elf(ifs);
}
ssize_t elf_binary_size(const std::string& filename) {
std::ifstream ifs(filename);
if (!ifs) {
log_error("could not open file\n");
return -1;
}
if (is_32bit_elf(ifs)) {
return get_elf_size<Elf32_Ehdr, Elf32_Shdr>(ifs);
} else {
return get_elf_size<Elf64_Ehdr, Elf64_Shdr>(ifs);
}
}
07070100000004000081a400000000000000000000000168cf6940000002a7000000000000000000000000000000000000001800000000src/binfmt-bypass/elf.h#pragma once
#include <string>
/**
* Check whether file is linked staticallly.
* @param filename path to ELF file
* @return true if file is statically linked, false otherwise
*/
bool is_statically_linked_elf(const std::string& filename);
/**
* Calculate size of ELF binary. Useful e.g., to estimate the size of the runtime in an AppImage.
* @param filename path to ELF file
* @return size of ELF part in bytes
*/
ssize_t elf_binary_size(const std::string& filename);
/**
* Check whether a given ELF file is a 32-bit binary.
* @param filename path to ELF file
* @return true if it's a 32-bit ELF, false otherwise
*/
bool is_32bit_elf(const std::string& filename);
07070100000005000081a400000000000000000000000168cf6940000004f4000000000000000000000000000000000000002900000000src/binfmt-bypass/fix-preload-library.sh#! /bin/bash
set -euo pipefail
# 2.17 ensures compatibility with CentOS 7 and beyond
# cf. https://gist.github.com/wagenet/35adca1a032cec2999d47b6c40aa45b1
glibc_ok_version="2.17"
find_too_new_symbols() {
glibc_symbols=( "$(nm --dynamic --undefined-only --with-symbol-versions "$1" | grep "GLIBC_")" )
for glibc_symbol in "${glibc_symbols[@]}"; do
# shellcheck disable=SC2001
glibc_symbol_version="$(sed 's|.*GLIBC_\([\.0-9]\+\)$|\1|' <<< "$glibc_symbol")"
newest_glibc_symbol_version="$(echo -e "$glibc_ok_version\\n$glibc_symbol_version" | sort -V | tail -n1)"
# make sure the newest version found is <= the one we define as ok
if [[ "$newest_glibc_symbol_version" == "$glibc_ok_version" ]]; then
return 1
fi
done
return 0
}
for file in "$@"; do
# obviously, this is a hack, but it should work well enough since we just need to do it for one single symbol from libdl
patchelf --debug --clear-symbol-version dlsym "$file"
nm_data="$(nm --dynamic --undefined-only --with-symbol-versions "$file")"
if find_too_new_symbols "$file"; then
echo "Error: found symbol version markers newer than $glibc_ok_version:"
echo "$nm_data"
exit 1
fi
done
07070100000006000081a400000000000000000000000168cf69400000075c000000000000000000000000000000000000002700000000src/binfmt-bypass/interpreter_main.cpp// system headers
#include <cassert>
#include <unistd.h>
#include <vector>
// own headers
#include "logging.h"
#include "lib.h"
bool executableExists(const std::string& path) {
if (access(path.c_str(), X_OK) != 0) {
log_debug("executable %s does not exist\n", path.c_str());
return false;
}
return true;
}
int main(int argc, char** argv) {
log_debug("Welcome to AppImageLauncher's binfmt_misc interpreter!\n");
if (argc <= 1) {
log_message("Usage: %s <AppImage file> [args...]\n", argv[0]);
return EXIT_CODE_FAILURE;
}
const std::string appImagePath = argv[1];
std::vector<char*> args(&argv[2], &argv[argc]);
// optimistic approach
bool useAppImageLauncher = true;
if (!executableExists(APPIMAGELAUNCHER_PATH)) {
log_message(
"AppImageLauncher not found at %s, launching AppImage directly: %s\n",
APPIMAGELAUNCHER_PATH,
appImagePath.c_str()
);
useAppImageLauncher = false;
}
if (getenv("APPIMAGELAUNCHER_DISABLE") != nullptr) {
log_message(
"APPIMAGELAUNCHER_DISABLE set, launching AppImage directly: %s\n",
APPIMAGELAUNCHER_PATH,
appImagePath.c_str()
);
useAppImageLauncher = false;
}
if (!useAppImageLauncher) {
return bypassBinfmtAndRunAppImage(argv[1], args);
}
log_debug(
"AppImageLauncher found at %s, launching AppImage %s with it\n",
APPIMAGELAUNCHER_PATH,
appImagePath.c_str()
);
// needs to be done in inverse order
args.emplace(args.begin(), strdup(appImagePath.c_str()));
args.emplace(args.begin(), strdup(APPIMAGELAUNCHER_PATH));
const auto rv = execv(APPIMAGELAUNCHER_PATH, args.data());
assert(rv == -1);
log_error("execv(%s, ...) failed\n");
return EXIT_CODE_FAILURE;
}
07070100000007000081a400000000000000000000000168cf694000002bd7000000000000000000000000000000000000001a00000000src/binfmt-bypass/lib.cpp// system headers
#include <cstdio>
#include <sys/mman.h>
#include <unistd.h>
#include <fcntl.h>
#include <wait.h>
#include <vector>
#include <memory.h>
#include <memory>
#include <stdexcept>
#include <cassert>
#include <filesystem>
// own headers
#include "elf.h"
#include "logging.h"
#include "lib.h"
#include "binfmt-bypass-preload.h"
#ifdef PRELOAD_LIB_NAME_32BIT
#include "binfmt-bypass-preload_32bit.h"
#endif
#define EXIT_CODE_FAILURE 0xff
bool copy_and_patch_runtime(int fd, const char* const appimage_filename, const ssize_t elf_size) {
// copy runtime header into memfd "file"
{
const auto realfd = open(appimage_filename, O_RDONLY);
std::vector<char> buffer(elf_size);
// TODO: check for errors
read(realfd, buffer.data(), elf_size);
write(fd, buffer.data(), elf_size);
close(realfd);
}
// erase magic bytes
lseek(fd, 8, SEEK_SET);
char null_buf[]{0, 0, 0};
write(fd, null_buf, 3);
// TODO: handle errors properly
return true;
}
#ifdef HAVE_MEMFD_CREATE
// if memfd_create is available, we should use it as it has a few important advantages over older solutions like
// shm_open or classic tempfiles
int create_memfd_with_patched_runtime(const char* const appimage_filename, const ssize_t elf_size) {
// as we call exec() after fork() to create a child process (the parent keeps it alive, the child doesn't require
// access anyway), we enable close-on-exec
const auto memfd = memfd_create("runtime", MFD_CLOEXEC);
if (memfd < 0) {
log_error("memfd_create failed: %s\n", strerror(errno));
return -1;
}
if (!copy_and_patch_runtime(memfd, appimage_filename, elf_size)) {
log_error("failed to copy and patch runtime\n");
close(memfd);
return -1;
}
return memfd;
}
#else
// in case memfd_create is *not* available, we fall back to shm_open
// it requires a few more lines of code (e.g., changing permissions to limit access to the created file)
// also, we can't just
int create_shm_fd_with_patched_runtime(const char* const appimage_filename, const ssize_t elf_size) {
// let's hope that mktemp returns a unique filename; if not, shm_open returns an error, thanks to O_EXCL
// the file exists only for a fraction of a second normally, so the chances are not too bad
char mktemp_template[] = "runtime-XXXXXX";
const char* runtime_filename = mktemp(mktemp_template);
if (runtime_filename[0] == '\0') {
log_error("failed to create temporary filename\n");
return -1;
}
// shm_open doesn't survive over exec(), so we _have to_ keep this process alive and create a child for the runtime
// the good news is: we don't have to worry about setting flags to close-on-exec
int writable_fd = shm_open(runtime_filename, O_RDWR | O_CREAT, 0700);
if (writable_fd < 0) {
log_error("shm_open failed (writable): %s\n", strerror(errno));
return -1;
}
// open file read-only before unlinking the file, this is the fd we return later
// otherwise we'll end up with ETXTBSY when trying to exec() it
int readable_fd = shm_open(runtime_filename, O_RDONLY, 0);
if (readable_fd < 0) {
log_error("shm_open failed (read-only): %s\n", strerror(errno));
return -1;
}
// let's make sure the file goes away when it's closed
// as long as we don't close the fd, it won't go away, but if we do, the OS takes care of freeing the memory
if (shm_unlink(runtime_filename) != 0) {
log_error("shm_unlink failed: %s\n", strerror(errno));
close(writable_fd);
return -1;
}
if (!copy_and_patch_runtime(writable_fd, appimage_filename, elf_size)) {
log_error("failed to copy and patch runtime\n");
close(writable_fd);
return -1;
}
// close writable fd and return readable one
close(writable_fd);
return readable_fd;
}
#endif
std::filesystem::path find_preload_library(bool is_32bit) {
// packaging is now done using ld-p-native_packages which does not make guarantees about the install path
// therefore, we need to look up the path of the preload library in relation to the current binary's path
// since we use the F (fix binary) binfmt mode nowadays to enable the use of the interpreter in different cgroups,
// namespaces or changeroots, we may not find the library there, but we'll at least try
// we expect the library to be placed next to this binary
const auto own_binary_path = std::filesystem::read_symlink("/proc/self/exe");
const auto dir_path = own_binary_path.parent_path();
std::filesystem::path rv = dir_path;
#ifdef PRELOAD_LIB_NAME_32BIT
if (is_32bit) {
rv /= PRELOAD_LIB_NAME_32BIT;
return rv;
}
#endif
rv /= PRELOAD_LIB_NAME;
return rv;
}
/**
* Create a temporary file within the shm file system and maintain its existence using the RAII principle.
* This is a first attempt, creating the files within /tmp. Future versions could try to put the files next to the
* AppImage, use a reproducible path to create the lib file just once, use shm_open etc.
*/
class TemporaryPreloadLibFile {
public:
TemporaryPreloadLibFile(const unsigned char* libContents, const std::streamsize libContentsSize) {
char tempFilePattern[] = "/tmp/appimagelauncher-preload-XXXXXX.so";
_fd = mkstemps(tempFilePattern, 3);
if (_fd == -1) {
throw std::runtime_error("could not create temporary preload lib file");
}
_path = tempFilePattern;
if (write(_fd, libContents, libContentsSize) != libContentsSize) {
throw std::runtime_error("failed to write contents to temporary preload lib");
}
}
~TemporaryPreloadLibFile() {
close(_fd);
unlink(_path.c_str());
};
std::string path() {
return _path;
}
private:
int _fd;
std::filesystem::path _path;
};
// need to keep track of the subprocess pid in a global variable, as signal handlers in C(++) are simple interrupt
// handlers that are not aware of any state in main()
// note that we only connect the signal handler once we have created a subprocess, i.e., we don't need to worry about
// subprocess_pid not being set yet
// it's best practice to implement a check anyway, though
static pid_t subprocess_pid = 0;
void forwardSignal(int signal) {
if (subprocess_pid != 0) {
log_debug("forwarding signal %d to subprocess %ld\n", signal, subprocess_pid);
kill(subprocess_pid, signal);
} else {
log_error("signal %d received but no subprocess created yet, shutting down\n", signal);
exit(signal);
}
}
int bypassBinfmtAndRunAppImage(const std::string& appimage_path, const std::vector<char*>& target_args) {
// read size of AppImage runtime (i.e., detect size of ELF binary)
const auto size = elf_binary_size(appimage_path.c_str());
if (size < 0) {
log_error("failed to detect runtime size\n");
return EXIT_CODE_FAILURE;
}
#ifdef HAVE_MEMFD_CREATE
// create "file" in memory, copy runtime there and patch out magic bytes
int runtime_fd = create_memfd_with_patched_runtime(appimage_path.c_str(), size);
#else
int runtime_fd = create_shm_fd_with_patched_runtime(appimage_filename.c_str(), size);
#endif
if (runtime_fd < 0) {
log_error("failed to set up in-memory file with patched runtime\n");
return EXIT_CODE_FAILURE;
}
// to keep alive the memfd, we launch the AppImage as a subprocess
if ((subprocess_pid = fork()) == 0) {
// create new argv array, using passed filename as argv[0]
std::vector<char*> new_argv;
new_argv.push_back(strdup(appimage_path.c_str()));
// insert remaining args, if any
for (const auto& arg : target_args) {
new_argv.push_back(strdup(arg));
}
// needs to be null terminated, of course
new_argv.push_back(nullptr);
// preload our library
auto preload_lib_path = find_preload_library(is_32bit_elf(appimage_path));
log_debug("preload lib path: %s\n", preload_lib_path.string().c_str());
// may or may not be used, but must survive until this application terminates
std::unique_ptr<TemporaryPreloadLibFile> temporaryPreloadLibFile;
if (!std::filesystem::exists(preload_lib_path)) {
log_warning("could not find preload library, creating new temporary file for it\n");
#ifdef PRELOAD_LIB_NAME_32BIT
if (is_32bit_elf(appimage_path)) {
temporaryPreloadLibFile = std::make_unique<TemporaryPreloadLibFile>(
libbinfmt_bypass_preload_32bit_so,
libbinfmt_bypass_preload_32bit_so_len
);
}
#endif
if (temporaryPreloadLibFile == nullptr) {
temporaryPreloadLibFile = std::make_unique<TemporaryPreloadLibFile>(
libbinfmt_bypass_preload_so,
libbinfmt_bypass_preload_so_len
);
}
assert(temporaryPreloadLibFile != nullptr);
preload_lib_path = temporaryPreloadLibFile->path();
}
if (!is_statically_linked_elf(appimage_path)) {
log_debug("library to preload: %s\n", preload_lib_path.string().c_str());
setenv("LD_PRELOAD", preload_lib_path.c_str(), true);
}
// calculate absolute path to AppImage, for use in the preloaded lib
char* abs_appimage_path = realpath(appimage_path.c_str(), nullptr);
log_debug("absolute AppImage path: %s\n", abs_appimage_path);
// TARGET_APPIMAGE is further needed for static runtimes which do not make any use of LD_PRELOAD
setenv("REDIRECT_APPIMAGE", abs_appimage_path, true);
setenv("TARGET_APPIMAGE", abs_appimage_path, true);
// launch memfd directly, no path needed
log_debug("fexecve(...)\n");
fexecve(runtime_fd, new_argv.data(), environ);
log_error("failed to execute patched runtime: %s\n", strerror(errno));
return EXIT_CODE_FAILURE;
}
// now that we have a subprocess and know its process ID, it's time to set up signal forwarding
// note that from this point on, we don't handle signals ourselves any more, but rely on the subprocess to exit
// properly
for (int i = 0; i < 32; ++i) {
signal(i, forwardSignal);
}
// wait for child process to exit, and exit with its return code
int status;
wait(&status);
// clean up
close(runtime_fd);
// calculate return code based on child's behavior
int child_retcode;
if (WIFSIGNALED(status) != 0) {
child_retcode = WTERMSIG(status);
log_error("child exited with code %d\n", child_retcode);
} else if (WIFEXITED(status) != 0) {
child_retcode = WEXITSTATUS(status);
log_debug("child exited normally with code %d\n", child_retcode);
} else {
log_error("unknown error: child didn't exit with signal or regular exit code\n");
child_retcode = EXIT_CODE_FAILURE;
}
return child_retcode;
}
07070100000008000081a400000000000000000000000168cf6940000000ce000000000000000000000000000000000000001800000000src/binfmt-bypass/lib.h#pragma once
// system headers
#include <string>
#include <vector>
#define EXIT_CODE_FAILURE 0xff
int bypassBinfmtAndRunAppImage(const std::string& appimage_path, const std::vector<char*>& target_args);
07070100000009000081a400000000000000000000000168cf694000000679000000000000000000000000000000000000001c00000000src/binfmt-bypass/logging.h#pragma once
// system headers
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifndef COMPONENT_NAME
#error component name undefined
#endif
static int v_log_message(const char* const format, va_list args) {
static const char prefix[] = "[appimagelauncher-binfmt-bypass/" COMPONENT_NAME "] ";
char* patched_format = (char*) (malloc(strlen(format) + strlen(prefix) + 1));
strcpy(patched_format, prefix);
strcat(patched_format, format);
return vfprintf(stderr, patched_format, args);
}
static int v_log_message_prefix(const char* const prefix, const char* const format, va_list args) {
char* patched_format = (char*) (malloc(strlen(format) + strlen(prefix) + 2 + 1));
strcpy(patched_format, prefix);
strcat(patched_format, ": ");
strcat(patched_format, format);
return v_log_message(patched_format, args);
}
static int log_message(const char* const format, ...) {
va_list args;
va_start(args, format);
int result = v_log_message(format, args);
va_end(args);
return result;
}
static void log_debug(const char* const format, ...) {
if (getenv("DEBUG") == NULL) {
return;
}
va_list args;
va_start(args, format);
v_log_message_prefix("DEBUG", format, args);
va_end(args);
}
static void log_error(const char* const format, ...) {
va_list args;
va_start(args, format);
v_log_message_prefix("ERROR", format, args);
va_end(args);
}
static void log_warning(const char* const format, ...) {
va_list args;
va_start(args, format);
v_log_message_prefix("WARNING", format, args);
va_end(args);
}
0707010000000a000081a400000000000000000000000168cf6940000011dd000000000000000000000000000000000000001c00000000src/binfmt-bypass/preload.c// system headers
#include <stdio.h>
#include <dlfcn.h>
#include <unistd.h>
#include <memory.h>
#include <stdlib.h>
#include <fcntl.h>
#include <limits.h>
#include <errno.h>
#include <stdbool.h>
// own headers
#include "logging.h"
// saw this trick somewhere on the Internet... don't recall where it was, but it works well
#ifndef RTLD_NEXT
#define RTLD_NEXT ((void*) -1l)
#endif
#define REAL_LIBC RTLD_NEXT
// TODO: move into central header, it's the same value in main.cpp
#define EXIT_CODE_FAILURE 0xff
// pointers to actual implementations in libc
// will be initialized by __init()
// TODO: create a macro for this pattern (DRY)
static char* (*__libc_realpath)(const char*, char*) = NULL;
static int (*__libc_open)(const char*, int) = NULL;
static ssize_t (*__libc_readlink)(const char*, void*, size_t) = NULL;
// TODO: write __init() and call that from all functions, loading all required symbols, the AppImage path etc. once
// to improve performance
// DRY
static const char proc_self_exe[] = "/proc/self/exe";
void __init() {
static bool initialized = false;
if (!initialized) {
initialized = true;
// get rid of $LD_PRELOAD in the first binary which this library is preloaded into (should be the runtime)
// the easiest way is to wait for one of these functions to be used, then unset it
unsetenv("LD_PRELOAD");
// load symbols from libc
__libc_readlink = (ssize_t (*) (const char*, void*, size_t)) dlsym(REAL_LIBC, "readlink");
__libc_realpath = (char* (*) (const char*, char*)) dlsym(REAL_LIBC, "realpath");
__libc_open = (int (*) (const char*, int)) dlsym(REAL_LIBC, "open");
if (__libc_readlink == NULL || __libc_realpath == NULL || __libc_open == NULL) {
log_error("failed to load symbol from libc\n");
exit(EXIT_CODE_FAILURE);
}
}
}
char* __abs_appimage_path() {
__init();
static const char env_var_name[] = "TARGET_APPIMAGE";
char* appimage_var = getenv(env_var_name);
if (appimage_var == NULL || appimage_var[0] == '\0') {
log_error("$%s not set\n", env_var_name);
exit(EXIT_CODE_FAILURE);
}
// make path absolute if needed (best effort, it's better to pass an absolute value)
if (appimage_var[0] != '/') {
log_warning("$%s value is not absolute, trying to make it absolute\n", env_var_name);
char* abspath = calloc(PATH_MAX, sizeof(char));
if (readlink(appimage_var, abspath, sizeof(abspath)) == -1) {
log_error("readlink failed on %s: %s\n", appimage_var, strerror(errno));
exit(EXIT_CODE_FAILURE);
}
return abspath;
}
return strdup(appimage_var);
}
__attribute__((visibility ("default")))
extern ssize_t readlink(const char* path, char* buf, size_t len) {
__init();
log_debug("readlink %s, len %ld\n", path, len);
if (strncmp(path, proc_self_exe, strlen(proc_self_exe)) == 0) {
char* abspath = __abs_appimage_path();
log_debug("redirecting readlink to %s\n", abspath);
size_t ret = strlen(abspath);
strncpy(buf, abspath, ret);
log_debug("buf: %s, len: %ld\n", buf, ret);
free(abspath);
return ret;
}
return __libc_readlink(path, buf, len);
}
__attribute__((visibility ("default")))
extern char* realpath(const char* name, char* resolved) {
__init();
log_debug("realpath %s, %s\n", name, resolved);
if (strncmp(name, proc_self_exe, strlen(proc_self_exe)) == 0) {
char* appimage = __abs_appimage_path();
log_debug("changing realpath destination to %s\n", appimage);
if (resolved == NULL) {
resolved = appimage;
} else {
strncpy(resolved, appimage, PATH_MAX);
free(appimage);
}
return resolved;
}
char* retval = __libc_realpath(name, resolved);
log_debug("realpath result: %s -> %s, retval %s\n", name, resolved, retval);
return retval;
}
// used by squashfuse, specifically util.c/sqfs_fd_open
__attribute__((visibility ("default")))
extern int open(const char* file, int flags, ...) {
__init();
log_debug("open(%s, %d)\n", file, flags);
char* abspath = NULL;
if (strncmp(file, proc_self_exe, strlen(proc_self_exe)) == 0) {
abspath = __abs_appimage_path();
log_debug("redirecting open to %s\n", abspath);
file = abspath;
}
int result = __libc_open(file, flags);
if (abspath != NULL) {
free(abspath);
}
return result;
}
0707010000000b000041ed00000000000000000000000168cf694000000000000000000000000000000000000000000000001200000000src/binfmt-bypass0707010000000c000081a400000000000000000000000168cf69400000013e000000000000000000000000000000000000001700000000src/cli/CMakeLists.txtadd_subdirectory(logging)
add_subdirectory(commands)
add_executable(ail-cli cli_main.cpp)
target_link_libraries(ail-cli PUBLIC cli_commands cli_logging)
set_property(
TARGET ail-cli
PROPERTY INSTALL_RPATH ${_rpath}
)
install(
TARGETS ail-cli
DESTINATION ${_bindir} COMPONENT APPIMAGELAUNCHER_CLI
)
0707010000000d000081a400000000000000000000000168cf6940000008a0000000000000000000000000000000000000001500000000src/cli/cli_main.cpp// system headers
#include <sstream>
// library headers
#include <QCoreApplication>
#include <QCommandLineParser>
// local headers
#include "CommandFactory.h"
#include "exceptions.h"
#include "logging.h"
using namespace appimagelauncher::cli;
using namespace appimagelauncher::cli::commands;
int main(int argc, char** argv) {
// we don't have support for UI (and don't want that), so let's tell shared to not display dialogs
setenv("_FORCE_HEADLESS", "1", 1);
QCoreApplication app(argc, argv);
std::ostringstream version;
version << "version " << APPIMAGELAUNCHER_VERSION << " "
<< "(git commit " << APPIMAGELAUNCHER_GIT_COMMIT << "), built on "
<< APPIMAGELAUNCHER_BUILD_DATE;
QCoreApplication::setApplicationVersion(QString::fromStdString(version.str()));
QCommandLineParser parser;
parser.addPositionalArgument("<command>", "Command to run (see help for more information");
parser.addPositionalArgument("[...]", "command-specific additional arguments");
parser.addHelpOption();
parser.addVersionOption();
parser.process(app);
auto posArgs = parser.positionalArguments();
if (posArgs.isEmpty()) {
qerr() << parser.helpText().toStdString().c_str() << endl;
qerr() << "Available commands:" << endl;
qerr() << " integrate Integrate AppImages passed as commandline arguments" << endl;
qerr() << " unintegrate Unintegrate AppImages passed as commandline arguments" << endl;
qerr() << " would-integrate Report whether AppImage would be integrated (exits with 0 if yes, any other code if not)" << endl;
return 2;
}
auto commandName = posArgs.front();
posArgs.pop_front();
try {
auto command = CommandFactory::getCommandByName(commandName);
command->exec(posArgs);
} catch (const CommandNotFoundError& e) {
qerr() << e.what() << endl;
return 1;
} catch (const InvalidArgumentsError& e) {
qerr() << "Invalid arguments: " << e.what() << endl;
return 3;
} catch (const UsageError& e) {
qerr() << "Usage error: " << e.what() << endl;
return 3;
}
return 0;
}
0707010000000e000081a400000000000000000000000168cf69400000019b000000000000000000000000000000000000002000000000src/cli/commands/CMakeLists.txtadd_library(cli_commands STATIC
Command.h
CommandFactory.h CommandFactory.cpp
exceptions.h
IntegrateCommand.h IntegrateCommand.cpp
UnintegrateCommand.h UnintegrateCommand.cpp
WouldIntegrateCommand.h WouldIntegrateCommand.cpp
)
target_link_libraries(cli_commands PUBLIC Qt5::Core shared cli_logging libappimage)
target_include_directories(cli_commands PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
0707010000000f000081a400000000000000000000000168cf6940000001cc000000000000000000000000000000000000001b00000000src/cli/commands/Command.h#pragma once
// system headers
#include <memory>
// library headers
#include <QList>
#include <QString>
namespace appimagelauncher {
namespace cli {
namespace commands {
/**
* Base class for CLI command implementations.
*/
class Command {
public:
// Run the command.
virtual void exec(QList<QString> arguments) = 0;
};
}
}
}
07070100000010000081a400000000000000000000000168cf694000000346000000000000000000000000000000000000002400000000src/cli/commands/CommandFactory.cpp// local headers
#include "CommandFactory.h"
#include "IntegrateCommand.h"
#include "UnintegrateCommand.h"
#include "WouldIntegrateCommand.h"
#include "exceptions.h"
namespace appimagelauncher {
namespace cli {
namespace commands {
std::shared_ptr<Command> CommandFactory::getCommandByName(const QString& commandName) {
if (commandName == "integrate") {
return std::shared_ptr<Command>(new IntegrateCommand);
} else if (commandName == "unintegrate") {
return std::make_shared<UnintegrateCommand>();
} else if (commandName == "would-integrate") {
return std::make_shared<WouldIntegrateCommand>();
}
throw CommandNotFoundError(commandName);
}
}
}
}
07070100000011000081a400000000000000000000000168cf694000000197000000000000000000000000000000000000002200000000src/cli/commands/CommandFactory.h#pragma once
// system headers
#include <memory>
// library headers
#include <QString>
// local headers
#include <Command.h>
/**
* Creates Commands.
*/
namespace appimagelauncher {
namespace cli {
namespace commands {
class CommandFactory {
public:
static std::shared_ptr<Command> getCommandByName(const QString&);
};
}
}
}
07070100000012000081a400000000000000000000000168cf694000000ee6000000000000000000000000000000000000002600000000src/cli/commands/IntegrateCommand.cpp// library headers
#include <QFileInfo>
// local headers
#include "IntegrateCommand.h"
#include "exceptions.h"
#include "shared.h"
#include "logging.h"
namespace appimagelauncher {
namespace cli {
namespace commands {
void IntegrateCommand::exec(QList<QString> arguments) {
if (arguments.empty()) {
throw InvalidArgumentsError("No AppImages passed on commandline");
}
// make sure all AppImages exist on disk before further processing
for (auto& path : arguments) {
if (!QFileInfo(path).exists()) {
throw UsageError("could not find file " + path);
}
// make path absolute
// that will just prevent mistakes in libappimage and shared etc.
// (stuff like TryExec keys etc. being set to paths relative to CWD when running the command , ...)
path = QFileInfo(path).absoluteFilePath();
}
for (const auto& pathToAppImage : arguments) {
qout() << "Processing " << pathToAppImage << endl;
if (!QFileInfo(pathToAppImage).isFile()) {
qerr() << "Warning: Not a file, skipping: " << pathToAppImage << endl;
continue;
}
if (!isAppImage(pathToAppImage)) {
qerr() << "Warning: Not an AppImage, skipping: " << pathToAppImage << endl;
continue;
}
if (hasAlreadyBeenIntegrated(pathToAppImage)) {
if (desktopFileHasBeenUpdatedSinceLastUpdate(pathToAppImage)) {
qout() << "AppImage has been integrated already and doesn't need to be re-integrated, skipping" << endl;
continue;
}
qout() << "AppImage has already been integrated, but needs to be reintegrated" << endl;
}
auto pathToIntegratedAppImage = buildPathToIntegratedAppImage(pathToAppImage);
// make sure integration directory exists
// (important for new installations)
// pretty ugly, but well, one taketh what the Qt API giveth
QDir().mkdir(integratedAppImagesDestination().path());
// check if it's already in the right place
if (QFileInfo(pathToAppImage).absoluteFilePath() != QFileInfo(pathToIntegratedAppImage).absoluteFilePath()) {
qout() << "Moving AppImage to integration directory" << endl;
if (QFile::exists(pathToIntegratedAppImage) && !QFile(pathToIntegratedAppImage).remove()) {
qerr() << "Could not move AppImage into integration directory (error: failed to overwrite existing file)" << endl;
continue;
}
if (!QFile(pathToAppImage).rename(pathToIntegratedAppImage)) {
qerr() << "Cannot move AppImage to integration directory (permission problem?), attempting to copy instead" << endl;
if (!QFile(pathToAppImage).copy(pathToIntegratedAppImage)) {
throw CliError("Failed to copy AppImage, giving up");
}
}
} else {
qout() << "AppImage already in integration directory" << endl;
}
installDesktopFileAndIcons(pathToIntegratedAppImage);
}
}
}
}
}
07070100000013000081a400000000000000000000000168cf694000000181000000000000000000000000000000000000002400000000src/cli/commands/IntegrateCommand.h#pragma once
// local headers
#include "Command.h"
namespace appimagelauncher {
namespace cli {
namespace commands {
/**
* Integrates AppImages passed as arguments on the commandline.
*/
class IntegrateCommand : public Command {
void exec(QList<QString> arguments) final;
};
}
}
}
07070100000014000081a400000000000000000000000168cf69400000079a000000000000000000000000000000000000002800000000src/cli/commands/UnintegrateCommand.cpp// library headers
#include <QFileInfo>
// local headers
#include "UnintegrateCommand.h"
#include "exceptions.h"
#include "shared.h"
#include "logging.h"
namespace appimagelauncher {
namespace cli {
namespace commands {
void UnintegrateCommand::exec(QList<QString> arguments) {
if (arguments.empty()) {
throw InvalidArgumentsError("No AppImages passed on commandline");
}
// make sure all AppImages exist on disk before further processing
for (auto& path : arguments) {
if (!QFileInfo(path).exists()) {
throw UsageError("could not find file " + path);
}
// make path absolute
// that will just prevent mistakes in libappimage and shared etc.
// (stuff like TryExec keys etc. being set to paths relative to CWD when running the command , ...)
path = QFileInfo(path).absoluteFilePath();
}
for (const auto& pathToAppImage : arguments) {
qout() << "Processing " << pathToAppImage << endl;
if (!QFileInfo(pathToAppImage).isFile()) {
qerr() << "Warning: Not a file, skipping: " << pathToAppImage << endl;
continue;
}
if (!isAppImage(pathToAppImage)) {
qerr() << "Warning: Not an AppImage, skipping: " << pathToAppImage << endl;
continue;
}
if (!hasAlreadyBeenIntegrated(pathToAppImage)) {
qout() << "AppImage has not been integrated yet, skipping" << endl;
continue;
}
unregisterAppImage(pathToAppImage);
}
}
}
}
}
07070100000015000081a400000000000000000000000168cf694000000183000000000000000000000000000000000000002600000000src/cli/commands/UnintegrateCommand.h#pragma once
// local headers
#include "Command.h"
namespace appimagelauncher {
namespace cli {
namespace commands {
/**
* Integrates AppImages passed as arguments on the commandline.
*/
class UnintegrateCommand : public Command {
void exec(QList<QString> arguments) final;
};
}
}
}
07070100000016000081a400000000000000000000000168cf694000000e5b000000000000000000000000000000000000002b00000000src/cli/commands/WouldIntegrateCommand.cpp// library headers
#include <QFileInfo>
extern "C" {
#include <appimage/appimage.h>
}
// local headers
#include "WouldIntegrateCommand.h"
#include "exceptions.h"
#include "shared.h"
#include "logging.h"
namespace appimagelauncher {
namespace cli {
namespace commands {
void WouldIntegrateCommand::exec(QList<QString> arguments) {
if (arguments.empty()) {
throw InvalidArgumentsError("No AppImages passed on commandline");
}
// make sure all AppImages exist on disk before further processing
for (auto& path : arguments) {
if (!QFileInfo(path).exists()) {
throw UsageError("could not find file " + path);
}
// make path absolute
// that will just prevent mistakes in libappimage and shared etc.
// (stuff like TryExec keys etc. being set to paths relative to CWD when running the command , ...)
path = QFileInfo(path).absoluteFilePath();
}
for (const auto& pathToAppImage : arguments) {
qout() << "Checking whether " << pathToAppImage << " should be integrated" << endl;
if (!QFileInfo(pathToAppImage).isFile()) {
qerr() << "Warning: Not a file, skipping: " << pathToAppImage << endl;
continue;
}
if (!isAppImage(pathToAppImage)) {
qerr() << "Warning: Not an AppImage, skipping: " << pathToAppImage << endl;
continue;
}
// TODO: refactor into a function that, e.g., returns an enum
if (hasAlreadyBeenIntegrated(pathToAppImage)) {
if (desktopFileHasBeenUpdatedSinceLastUpdate(pathToAppImage)) {
throw WouldNotIntegrateError("AppImage has been integrated already and doesn't need to be re-integrated");
}
qout() << "AppImage has already been integrated, but needs to be reintegrated" << endl;
}
// check for X-AppImage-Integrate=false
auto shallNotBeIntegrated = appimage_shall_not_be_integrated(pathToAppImage.toStdString().c_str());
if (shallNotBeIntegrated < 0) {
throw CliError("AppImageLauncher error: appimage_shall_not_be_integrated() failed (returned " + QString::number(shallNotBeIntegrated) + ")");
} else if (shallNotBeIntegrated > 0) {
throw WouldNotIntegrateError("AppImage should not be integrated");
}
if (pathToAppImage.startsWith("/tmp/.mount_")) {
throw WouldNotIntegrateError("AppImages in AppImages are not supposed to be integrated");
}
// ignore terminal apps (fixes #2)
auto isTerminalApp = appimage_is_terminal_app(pathToAppImage.toStdString().c_str());
if (isTerminalApp < 0) {
throw CliError("AppImageLauncher error: appimage_is_terminal_app() failed (returned " + QString::number(isTerminalApp) + ")");
} else if (isTerminalApp > 0) {
throw WouldNotIntegrateError("Terminal AppImages should not be integrated");
}
qerr() << "AppImage should be integrated" << endl;
}
}
}
}
}
07070100000017000081a400000000000000000000000168cf694000000178000000000000000000000000000000000000002900000000src/cli/commands/WouldIntegrateCommand.h#pragma once
// local headers
#include "Command.h"
namespace appimagelauncher {
namespace cli {
namespace commands {
/**
* Check whether an AppImage would be integrated.
*/
class WouldIntegrateCommand : public Command {
void exec(QList<QString> arguments) final;
};
}
}
}
07070100000018000081a400000000000000000000000168cf6940000004fc000000000000000000000000000000000000001e00000000src/cli/commands/exceptions.h#pragma once
// system headers
#include <stdexcept>
// library headers
#include <QString>
namespace appimagelauncher {
namespace cli {
namespace commands {
class CliError : public std::runtime_error {
public:
explicit CliError(const QString& message) : std::runtime_error(message.toStdString()) {}
};
class CommandNotFoundError : public std::runtime_error {
private:
QString commandName;
public:
explicit CommandNotFoundError(const QString& commandName) : std::runtime_error(
"No such command available: " + commandName.toStdString()), commandName(commandName) {}
QString getCommandName() const {
return commandName;
}
};
class UsageError : public CliError {
public:
using CliError::CliError;
};
class InvalidArgumentsError : public UsageError {
public:
using UsageError::UsageError;
};
class WouldNotIntegrateError : public CliError {
public:
using CliError::CliError;
};
}
}
}
07070100000019000041ed00000000000000000000000168cf694000000000000000000000000000000000000000000000001100000000src/cli/commands0707010000001a000081a400000000000000000000000168cf694000000087000000000000000000000000000000000000001f00000000src/cli/logging/CMakeLists.txtadd_library(cli_logging INTERFACE)
set_property(TARGET cli_logging PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR})
0707010000001b000081a400000000000000000000000168cf694000000111000000000000000000000000000000000000001a00000000src/cli/logging/logging.h#pragma once
// system headers
#include <string.h>
// library headers
#include <QTextStream>
#include <QDebug>
// wrapper for stdout
#define qout() QTextStream(stdout, QIODevice::WriteOnly)
// wrapper for stderr
#define qerr() QTextStream(stderr, QIODevice::WriteOnly)
0707010000001c000041ed00000000000000000000000168cf694000000000000000000000000000000000000000000000001000000000src/cli/logging0707010000001d000041ed00000000000000000000000168cf694000000000000000000000000000000000000000000000000800000000src/cli0707010000001e000081a400000000000000000000000168cf6940000001a1000000000000000000000000000000000000001a00000000src/daemon/CMakeLists.txt# daemon binary
add_executable(appimagelauncherd main.cpp daemon.cpp worker.cpp)
target_link_libraries(appimagelauncherd shared filesystemwatcher PkgConfig::glib libappimage)
set_target_properties(appimagelauncherd PROPERTIES INSTALL_RPATH ${_rpath})
install(
TARGETS appimagelauncherd
RUNTIME DESTINATION ${_bindir} COMPONENT APPIMAGELAUNCHER
LIBRARY DESTINATION ${_libdir} COMPONENT APPIMAGELAUNCHER
)
0707010000001f000081a400000000000000000000000168cf6940000016b7000000000000000000000000000000000000001600000000src/daemon/daemon.cpp// STL headers
#include <chrono>
// library headers
#include <QDirIterator>
// local headers
#include "daemon.h"
#include "shared.h"
#include "appimage/appimage.h"
using namespace std::chrono_literals;
#define UPDATE_WATCHED_DIRECTORIES_INTERVAL 30s
namespace appimagelauncher::daemon {
Q_LOGGING_CATEGORY(daemonCat, "appimagelauncher.daemon")
Daemon::Daemon(QObject* parent) : QObject(parent), _settings(getConfig(this)), _worker(new Worker(this)),
_watcher(new FileSystemWatcher(this)), _updateWatchedDirsTimer(new QTimer(this)) {
// when we update the watched directories, the file system watcher can calculate whether there's new directories
// to watch these
QObject::connect(_watcher, &FileSystemWatcher::newDirectoriesToWatch, this, [this](const QDirSet& newDirs) {
if (newDirs.empty()) {
qCDebug(daemonCat) << "No new directories to watch detected";
} else {
qCInfo(daemonCat) << "Discovered new directories to watch, integrating existing AppImages initially";
initialSearchForAppImages(newDirs);
// (re-)integrate all AppImages at once
_worker->executeDeferredOperations();
}
});
// whenever a formerly watched directory disappears, we want to clean the menu from entries pointing to AppImages
// in this directory
// a good example for this situation is a removable drive that has been unplugged from the computer
QObject::connect(_watcher, &FileSystemWatcher::directoriesToWatchDisappeared, this, [](const QDirSet& disappearedDirs) {
if (disappearedDirs.empty()) {
qCDebug(daemonCat) << "No directories disappeared";
} else {
qCInfo(daemonCat) << "Directories to watch disappeared, unintegrating AppImages formerly found in there";
if (!cleanUpOldDesktopIntegrationResources(true)) {
qCCritical(daemonCat) << "Error: Failed to clean up old desktop integration resources";
}
}
});
// (re-)integrate all AppImages at once
_worker->executeDeferredOperations();
_updateWatchedDirsTimer->setInterval(UPDATE_WATCHED_DIRECTORIES_INTERVAL);
connect(
_updateWatchedDirsTimer, &QTimer::timeout, this,[this]() {
_watcher->updateWatchedDirectories(this->watchedDirectories());
}
);
_updateWatchedDirsTimer->start();
connect(_watcher, &FileSystemWatcher::fileChanged, _worker, &Worker::scheduleForIntegration,
Qt::QueuedConnection);
connect(_watcher, &FileSystemWatcher::fileRemoved, _worker, &Worker::scheduleForUnintegration,
Qt::QueuedConnection);
}
QDirSet Daemon::watchedDirectories() const {
return daemonDirectoriesToWatch(_settings);
}
void Daemon::initialSearchForAppImages(const QDirSet& dirsToSearch) {
// initial search for AppImages; if AppImages are found, they will be integrated, unless they already are
qCInfo(daemonCat) << "Searching for existing AppImages";
if (dirsToSearch.empty()) {
qCWarning(daemonCat) << "No directories to search provided initially, skipping";
return;
}
for (const auto& dir : dirsToSearch) {
if (!dir.exists()) {
qCDebug(daemonCat) << "Directory " << dir.path() << " does not exist, skipping";
continue;
}
qCInfo(daemonCat) << "Searching directory: " << dir.absolutePath();
for (QDirIterator it(dir); it.hasNext();) {
const auto& path = it.next();
if (QFileInfo(path).isFile()) {
const auto appImageType = appimage_get_type(path.toStdString().c_str(), false);
const auto isAppImage = 0 < appImageType && appImageType <= 2;
if (isAppImage) {
// at application startup, we don't want to integrate AppImages that have been integrated already,
// as that it slows down very much
// the integration will be updated as soon as any of these AppImages is run with AppImageLauncher
qCInfo(daemonCat) << "Found AppImage: " << path;
if (!appimage_is_registered_in_system(path.toStdString().c_str())) {
qCInfo(daemonCat) << "AppImage is not integrated yet, integrating";
_worker->scheduleForIntegration(path);
} else if (!desktopFileHasBeenUpdatedSinceLastUpdate(path)) {
qCInfo(daemonCat) << "AppImage has been integrated already but needs to be reintegrated";
_worker->scheduleForIntegration(path);
} else {
qCInfo(daemonCat) << "AppImage integrated already, skipping";
}
}
}
}
}
}
bool Daemon::startWatching() {
// make sure the watched directories list is up to date
_watcher->updateWatchedDirectories(watchedDirectories());
// search directories to watch once initially
// we *have* to do this even though we connect this signal above, as the first update occurs in the constructor
// and we cannot connect signals before construction has finished for obvious reasons
initialSearchForAppImages(_watcher->directories());
return _watcher->startWatching();
}
void Daemon::slotStopWatching() {
_watcher->stopWatching();
}
}
07070100000020000081a400000000000000000000000168cf694000000306000000000000000000000000000000000000001400000000src/daemon/daemon.h#pragma once
// library headers
#include <QObject>
#include <QSettings>
#include <QTimer>
#include <QLoggingCategory>
// local headers
#include "worker.h"
#include "types.h"
#include "filesystemwatcher.h"
namespace appimagelauncher::daemon {
Q_DECLARE_LOGGING_CATEGORY(daemonCat)
class Daemon : public QObject {
Q_OBJECT
public:
explicit Daemon(QObject* parent = nullptr);
QDirSet watchedDirectories() const;
bool startWatching();
public slots:
void slotStopWatching();
private:
void initialSearchForAppImages(const QDirSet& dirsToSearch);
QSettings *_settings;
Worker* _worker;
FileSystemWatcher *_watcher;
QTimer* _updateWatchedDirsTimer;
};
} // namespace
07070100000021000081a400000000000000000000000168cf6940000010e7000000000000000000000000000000000000001400000000src/daemon/main.cpp// system includes
#include <deque>
#include <iostream>
#include <sstream>
#include <sys/stat.h>
// library includes
#include <QCommandLineParser>
#include <QCoreApplication>
#include <QDebug>
#include <QDirIterator>
#include <QTimer>
#include <appimage/appimage.h>
// local includes
#include "shared.h"
#include "daemon.h"
using namespace appimagelauncher::daemon;
/**
* Read the modification time of the file pointed by <filePath>
* @param filePath
* @return file modification time
*/
long readFileModificationTime(char* filePath) {
struct stat attrib = {0x0};
stat(filePath, &attrib);
return attrib.st_ctime;
}
/**
* Monitors whether the application binary has changed since the process was started. In such case the application
* is restarted.
*
* @param argv
*/
QTimer* setupBinaryUpdatesMonitor(char* const* argv) {
auto* timer = new QTimer();
// It's used to restart the daemon if the binary changes
static const long binaryModificationTime = readFileModificationTime(argv[0]);
// callback to compare and restart the app if the binary changed since it was started
QObject::connect(timer, &QTimer::timeout, [argv]() {
long newBinaryModificationTime = readFileModificationTime(argv[0]);
if (newBinaryModificationTime != binaryModificationTime) {
std::cerr << "Binary file changed since the applications was started, proceeding to restart it."
<< std::endl;
execv(argv[0], argv);
}
});
// check every 5 min
timer->setInterval(5 * 60 * 1000);
return timer;
}
int main(int argc, char* argv[]) {
// make sure shared won't try to use the UI
setenv("_FORCE_HEADLESS", "1", 1);
// improve default logging format
qSetMessagePattern("[%{type}] %{category}: %{message}");
QCommandLineParser parser;
parser.setApplicationDescription(
QObject::tr(
"Tracks AppImages in applications directories (user's, system and other ones). "
"Automatically integrates AppImages moved into those directories and unintegrates ones removed from them."
)
);
QCommandLineOption listWatchedDirectoriesOption(
"list-watched-directories",
QObject::tr("Lists directories watched by this daemon and exit")
);
if (!parser.addOption(listWatchedDirectoriesOption)) {
throw std::runtime_error("could not add Qt command line option for some reason");
}
QCommandLineOption debugOption(
"debug",
QObject::tr("Enable debug logging")
);
if (!parser.addOption(debugOption)) {
throw std::runtime_error("could not add Qt command line option for some reason");
}
QCoreApplication app(argc, argv);
{
std::ostringstream version;
version << "version " << APPIMAGELAUNCHER_VERSION << " "
<< "(git commit " << APPIMAGELAUNCHER_GIT_COMMIT << "), built on "
<< APPIMAGELAUNCHER_BUILD_DATE;
QCoreApplication::setApplicationVersion(QString::fromStdString(version.str()));
}
// parse arguments
parser.process(app);
if (!parser.isSet(debugOption)) {
QLoggingCategory::setFilterRules("*.debug=false");
} else {
QLoggingCategory::setFilterRules("*.debug=true");
}
auto* daemon = new Daemon(&app);
// this option is for debugging the
if (parser.isSet(listWatchedDirectoriesOption)) {
for (const auto& watchedDir : daemon->watchedDirectories()) {
std::cout << watchedDir.absolutePath().toStdString() << std::endl;
}
return 0;
}
// after (re-)integrating all AppImages, clean up old desktop integration resources before start
if (!cleanUpOldDesktopIntegrationResources()) {
std::cout << "Failed to clean up old desktop integration resources" << std::endl;
}
qInfo() << "Watching directories:" << daemon->watchedDirectories();
if (!daemon->startWatching()) {
std::cerr << "Could not start watching directories" << std::endl;
return 1;
}
QCoreApplication::connect(&app, &QCoreApplication::aboutToQuit, daemon, &Daemon::slotStopWatching);
auto* binaryUpdatesMonitor = setupBinaryUpdatesMonitor(argv);
binaryUpdatesMonitor->start();
return QCoreApplication::exec();
}
07070100000022000081a400000000000000000000000168cf69400000194f000000000000000000000000000000000000001600000000src/daemon/worker.cpp// system includes
#include <atomic>
#include <iostream>
#include <deque>
// library includes
#include <QDebug>
#include <QFile>
#include <QObject>
#include <QSysInfo>
#include <QTimer>
#include <QThreadPool>
#include <QMutexLocker>
#include <appimage/appimage.h>
// local includes
#include "worker.h"
#include "shared.h"
namespace {
enum OP_TYPE {
INTEGRATE = 0,
UNINTEGRATE = 1,
};
typedef std::pair<QString, OP_TYPE> Operation;
}
namespace appimagelauncher::daemon {
Q_LOGGING_CATEGORY(workerCat, "appimagelauncher.daemon.worker")
class Worker::PrivateData {
public:
QTimer deferredOperationsTimer;
static constexpr int TIMEOUT = 15 * 1000;
// std::set is unordered, therefore using std::deque to keep the order of the operations
std::deque<Operation> deferredOperations;
class OperationTask : public QRunnable {
private:
Operation operation;
QMutex* mutex;
public:
OperationTask(const Operation& operation, QMutex* mutex) : operation(operation), mutex(mutex) {}
void run() override {
const auto& path = operation.first;
const auto& type = operation.second;
const auto exists = QFile::exists(path);
const auto appImageType = appimage_get_type(path.toStdString().c_str(), false);
const auto isAppImage = 0 < appImageType && appImageType <= 2;
if (type == INTEGRATE) {
{ // Scope for Output Mutex Locker
QMutexLocker mutexLocker(mutex);
std::cout << "Integrating: " << path.toStdString() << std::endl;
if (!exists) {
std::cout << "ERROR: file does not exist, cannot integrate" << std::endl;
return;
}
if (!isAppImage) {
std::cout << "ERROR: not an AppImage, skipping" << std::endl;
return;
}
}
// check for X-AppImage-Integrate=false
if (appimage_shall_not_be_integrated(path.toStdString().c_str())) {
QMutexLocker mutexLocker(mutex);
std::cout << "WARNING: AppImage shall not be integrated, skipping" << std::endl;
return;
}
if (!installDesktopFileAndIcons(path)) {
QMutexLocker mutexLocker(mutex);
std::cout << "ERROR: Failed to register AppImage in system" << std::endl;
return;
}
} else if (type == UNINTEGRATE) {
// nothing to do
}
}
};
public:
PrivateData() {
deferredOperationsTimer.setSingleShot(true);
deferredOperationsTimer.setInterval(TIMEOUT);
}
public:
// in addition to a simple duplicate check, this function is context sensitive
// it starts with the last element, and checks for duplicates until an opposite action is found
// for instance, when the element shall integrated, it will check for duplicates until an unintegration operation
// is found
bool isDuplicate(Operation operation) {
for (auto it = deferredOperations.rbegin(); it != deferredOperations.rend(); ++it) {
if ((*it).first == operation.first) {
// if operation type is different, then the operation is new, and should be added to the list
// if it is equal, it's a duplicate
// in either case, the loop can be aborted here
return (*it).second == operation.second;
}
}
return false;
}
};
Worker::Worker(QObject* parent) : QObject(parent) {
d = std::make_shared<PrivateData>();
connect(this, &Worker::startTimer, this, &Worker::startTimerIfNecessary, Qt::QueuedConnection);
connect(&d->deferredOperationsTimer, &QTimer::timeout, this, &Worker::executeDeferredOperations);
}
void Worker::executeDeferredOperations() {
if (d->deferredOperations.empty()) {
qCDebug(workerCat) << "No deferred operations to execute";
return;
}
std::cout << "Executing deferred operations" << std::endl;
QMutex outputMutex;
while (!d->deferredOperations.empty()) {
auto operation = d->deferredOperations.front();
d->deferredOperations.pop_front();
QThreadPool::globalInstance()->start(new PrivateData::OperationTask(operation, &outputMutex));
}
// wait until all AppImages have been integrated
QThreadPool::globalInstance()->waitForDone();
std::cout << "Cleaning up old desktop integration files" << std::endl;
if (!cleanUpOldDesktopIntegrationResources(true)) {
std::cout << "Failed to clean up old desktop integration files" << std::endl;
}
// make sure the icons in the launcher are refreshed
std::cout << "Updating desktop database and icon caches" << std::endl;
if (!updateDesktopDatabaseAndIconCaches())
std::cout << "Failed to update desktop database and icon caches" << std::endl;
std::cout << "Done" << std::endl;
}
void Worker::scheduleForIntegration(const QString& path) {
auto operation = std::make_pair(path, INTEGRATE);
if (!d->isDuplicate(operation)) {
std::cout << "Scheduling for (re-)integration: " << path.toStdString() << std::endl;
d->deferredOperations.push_back(operation);
emit startTimer();
}
}
void Worker::scheduleForUnintegration(const QString& path) {
auto operation = std::make_pair(path, UNINTEGRATE);
if (!d->isDuplicate(operation)) {
std::cout << "Scheduling for unintegration: " << path.toStdString() << std::endl;
d->deferredOperations.push_back(operation);
emit startTimer();
}
}
void Worker::startTimerIfNecessary() {
if (!d->deferredOperationsTimer.isActive())
QMetaObject::invokeMethod(&d->deferredOperationsTimer, "start");
}
}
07070100000023000081a400000000000000000000000168cf6940000002cd000000000000000000000000000000000000001400000000src/daemon/worker.h// system includes
#include <memory>
// library includes
#include <QObject>
#include <QLoggingCategory>
#pragma once
namespace appimagelauncher::daemon {
Q_DECLARE_LOGGING_CATEGORY(workerCat)
class Worker : public QObject {
Q_OBJECT
private:
class PrivateData;
std::shared_ptr<PrivateData> d = nullptr;
public:
explicit Worker(QObject* parent = nullptr);
signals:
void startTimer();
public slots:
void scheduleForIntegration(const QString& path);
void scheduleForUnintegration(const QString& path);
public slots:
void executeDeferredOperations();
private slots:
void startTimerIfNecessary();
};
}
07070100000024000041ed00000000000000000000000168cf694000000000000000000000000000000000000000000000000b00000000src/daemon07070100000025000081a400000000000000000000000168cf6940000000e2000000000000000000000000000000000000001d00000000src/fswatcher/CMakeLists.txtadd_library(filesystemwatcher STATIC filesystemwatcher.cpp filesystemwatcher.h)
target_link_libraries(filesystemwatcher PUBLIC Qt5::Core shared)
target_include_directories(filesystemwatcher PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
07070100000026000081a400000000000000000000000168cf694000002dad000000000000000000000000000000000000002400000000src/fswatcher/filesystemwatcher.cpp// system includes
#include <iostream>
#include <map>
#include <unistd.h>
// library includes
#include <QDir>
#include <QMutex>
#include <QTimer>
#include <QThread>
#include <sys/inotify.h>
// local includes
#include "filesystemwatcher.h"
namespace {
class INotifyEvent {
public:
uint32_t mask;
QString path;
public:
INotifyEvent(uint32_t mask, QString path) : mask(mask), path(std::move(path)) {}
};
}
namespace appimagelauncher::daemon {
Q_LOGGING_CATEGORY(fswCat, "appimagelauncher.daemon.filesystemwatcher")
class FileSystemWatcher::PrivateData {
public:
enum EVENT_TYPES {
// events that indicate file creations, modifications etc.
fileChangeEvents = IN_CLOSE_WRITE | IN_MOVE,
// events that indicate a file removal from a directory, e.g., deletion or moving to another location
fileRemovalEvents = IN_DELETE | IN_MOVED_FROM,
};
// tracks whether the watcher is running
bool isRunning;
public:
QDirSet watchedDirectories;
QTimer eventsLoopTimer;
QMutex* mutex;
private:
int inotifyFd = -1;
std::map<int, QDir> watchFdMap;
public:
// reads events from the inotify fd and emits the correct signals
std::vector<INotifyEvent> readEventsFromFd() {
// we don't want to read events in parallel
QMutexLocker lock{mutex};
// read raw bytes into buffer
// this is necessary, as the inotify_events have dynamic sizes
static const auto bufSize = 4096;
char buffer[bufSize] __attribute__ ((aligned(8)));
const auto rv = read(inotifyFd, buffer, bufSize);
const auto error = errno;
if (rv == 0) {
throw FileSystemWatcherError("read() on inotify FD must never return 0");
}
if (rv == -1) {
// we're using a non-blocking inotify fd, therefore, if errno is set to EAGAIN, we just didn't find any
// new events
// this is not an error case
if (error == EAGAIN)
return {};
throw FileSystemWatcherError(QString("Failed to read from inotify fd: ") + strerror(error));
}
// read events into vector
std::vector<INotifyEvent> events;
for (char* p = buffer; p < buffer + rv;) {
// create inotify_event from current position in buffer
auto* currentEvent = (struct inotify_event*) p;
// initialize new INotifyEvent with the data from the currentEvent
QString relativePath(currentEvent->name);
auto directory = watchFdMap[currentEvent->wd];
events.emplace_back(currentEvent->mask, directory.absolutePath() + "/" + relativePath);
// update current position in buffer
p += sizeof(struct inotify_event) + currentEvent->len;
}
return events;
}
PrivateData() : isRunning(false), watchedDirectories(), mutex(new QMutex) {
inotifyFd = inotify_init1(IN_NONBLOCK);
if (inotifyFd < 0) {
auto error = errno;
throw FileSystemWatcherError(QString("Failed to initialize inotify, reason: ") + strerror(error));
}
};
// caution: method is not threadsafe!
bool startWatching(const QDir& directory) {
static const auto mask = fileChangeEvents | fileRemovalEvents;
qCDebug(fswCat) << "start watching directory " << directory;
if (!directory.exists()) {
qCDebug(fswCat) << "Warning: directory " << directory.absolutePath() << " does not exist, skipping";
return true;
}
const int watchFd = inotify_add_watch(inotifyFd, directory.absolutePath().toStdString().c_str(), mask);
if (watchFd == -1) {
const auto error = errno;
qCCritical(fswCat) << "Failed to start watching: " << strerror(error);
return false;
}
watchFdMap[watchFd] = directory;
eventsLoopTimer.start();
return true;
}
bool startWatching() {
QMutexLocker lock{mutex};
for (const auto& directory: watchedDirectories) {
if (!startWatching(directory))
return false;
}
return true;
}
bool startWatching(const QDirSet& directories) {
QMutexLocker lock{mutex};
for (const auto& directory: directories) {
if (!startWatching(directory)) {
return false;
}
}
return true;
}
// caution: method is not threadsafe!
bool stopWatching(int watchFd) {
// no matter whether the watch removal succeeds, retrying to remove the watch won't help
// therefore, we can remove the file descriptor from the map in any case
watchFdMap.erase(watchFd);
qCDebug(fswCat) << "stop watching watchfd " << watchFd;
if (inotify_rm_watch(inotifyFd, watchFd) == -1) {
const auto error = errno;
qCCritical(fswCat) << "Failed to stop watching: " << strerror(error);
return false;
}
return true;
}
bool stopWatching() {
QMutexLocker lock{mutex};
while (!watchFdMap.empty()) {
const auto pair = *(watchFdMap.begin());
const auto watchFd = pair.first;
if (!stopWatching(watchFd)) {
qCCritical(fswCat) << "Warning: Failed to stop watching on file descriptor " << watchFd;
}
}
return true;
}
bool stopWatching(const QDirSet& directories) {
QMutexLocker lock{mutex};
for (const auto& directory: directories) {
for (const auto& pair: watchFdMap) {
if (pair.second == directory) {
if (!stopWatching(pair.first)) {
return false;
}
}
}
// reaching the following line means that we couldn't find the requested path in the fd map
return false;
}
return true;
}
};
FileSystemWatcher::FileSystemWatcher(QObject* parent) : QObject(parent) {
d = std::make_shared<PrivateData>();
d->eventsLoopTimer.setInterval(100);
connect(&d->eventsLoopTimer, &QTimer::timeout, this, &FileSystemWatcher::readEvents);
}
FileSystemWatcher::FileSystemWatcher(const QDir& path, QObject* parent) : FileSystemWatcher(parent) {
updateWatchedDirectories(QDirSet{{path}});
}
FileSystemWatcher::FileSystemWatcher(const QDirSet& paths, QObject* parent) : FileSystemWatcher(parent) {
updateWatchedDirectories(paths);
}
QDirSet FileSystemWatcher::directories() {
QMutexLocker lock{d->mutex};
return d->watchedDirectories;
}
bool FileSystemWatcher::startWatching() {
{
QMutexLocker lock{d->mutex};
if (d->isRunning) {
qCDebug(fswCat) << "tried to start file system watcher while it's running already";
return true;
}
}
auto rv = d->startWatching();
{
QMutexLocker lock{d->mutex};
if (rv)
d->isRunning = true;
}
return rv;
}
bool FileSystemWatcher::stopWatching() {
{
QMutexLocker lock{d->mutex};
if (!d->isRunning) {
qCDebug(fswCat) << "tried to stop file system watcher while stopped";
return true;
}
}
const auto rv = d->stopWatching();
{
QMutexLocker lock{d->mutex};
if (rv) {
d->isRunning = false;
// we can stop reporting events now, I guess
d->eventsLoopTimer.stop();
}
}
return rv;
}
void FileSystemWatcher::readEvents() {
auto events = d->readEventsFromFd();
for (const auto& event: events) {
const auto mask = event.mask;
if (mask & d->fileChangeEvents) {
emit fileChanged(event.path);
} else if (mask & d->fileRemovalEvents) {
emit fileRemoved(event.path);
}
}
}
bool FileSystemWatcher::updateWatchedDirectories(QDirSet watchedDirectories) {
// the list may contain entries for directories which don't exist already, therefore we have to remove those first
// so when they'll be created, we'll notice
{
// erase-remove doesn't work with sets apparently (see https://stackoverflow.com/a/26833313)
// therefore we use a simple linear search to remove non-existing directories
for (auto it = watchedDirectories.begin(); it != watchedDirectories.end(); ++it) {
if (!it->exists()) {
qCDebug(fswCat) << "Directory " << it->path() << " does not exist, skipping";
it = watchedDirectories.erase(it);
}
}
}
auto setDifference = [](const QDirSet& toExamine, const QDirSet& toSearchFor) -> QDirSet {
QDirSet results;
// QDir behaves weirdly with STL algorithm comparisons etc.
// therefore we implement this difference algorithm all by ourselves to make sure it works correctly
for (const auto& examined: toExamine) {
bool found = false;
for (const auto& searched: toSearchFor) {
if (searched == examined) {
found = true;
break;
}
}
if (!found) {
results.insert(examined);
}
}
return results;
};
// first, we calculate which directores are new to be watched
QDirSet newDirectories = setDifference(watchedDirectories, d->watchedDirectories);
// to stop watching with a fine granularity, we also need to know which directories have been removed
QDirSet disappearedDirectories = setDifference(d->watchedDirectories, watchedDirectories);
{
QMutexLocker lock{d->mutex};
// now we can update the internal state
d->watchedDirectories = watchedDirectories;
// if the watching hasn't been started yet, we shouldn't start/stop any watches
// unfortunately we need an extra variable to track this...
if (!d->isRunning)
return true;
}
// we must run both stop and start methods, so we cannot directly return false if either fails
// also, this makes sure the signals are sent even in case either of the following methods fails
bool rv = true;
rv = rv && d->stopWatching(disappearedDirectories);
rv = rv && d->startWatching(newDirectories);
// send out the signals for further handling by users of a fs watcher instance
emit newDirectoriesToWatch(newDirectories);
emit directoriesToWatchDisappeared(disappearedDirectories);
return rv;
}
}
07070100000027000081a400000000000000000000000168cf69400000055e000000000000000000000000000000000000002200000000src/fswatcher/filesystemwatcher.h// system includes
#include <algorithm>
#include <memory>
#include <unordered_set>
// library includes
#include <QDir>
#include <QObject>
#include <QLoggingCategory>
#include <QSet>
#include <QString>
#include <QThread>
// local includes
#include "types.h"
#pragma once
namespace appimagelauncher::daemon {
Q_DECLARE_LOGGING_CATEGORY(fswCat)
class FileSystemWatcherError : public std::runtime_error {
public:
explicit FileSystemWatcherError(const QString& message) : std::runtime_error(message.toStdString().c_str()) {};
};
class FileSystemWatcher : public QObject {
Q_OBJECT
private:
class PrivateData;
std::shared_ptr<PrivateData> d;
public:
explicit FileSystemWatcher(const QDir& directory, QObject* parent = nullptr);
explicit FileSystemWatcher(const QDirSet& paths, QObject* parent = nullptr);
explicit FileSystemWatcher(QObject* parent = nullptr);
public slots:
bool startWatching();
bool stopWatching();
void readEvents();
bool updateWatchedDirectories(QDirSet watchedDirectories);
public:
QDirSet directories();
signals:
void fileChanged(QString path);
void fileRemoved(QString path);
void newDirectoriesToWatch(QDirSet set);
void directoriesToWatchDisappeared(QDirSet set);
};
}
07070100000028000041ed00000000000000000000000168cf694000000000000000000000000000000000000000000000000e00000000src/fswatcher07070100000029000081a400000000000000000000000168cf694000000117000000000000000000000000000000000000001800000000src/i18n/CMakeLists.txtadd_library(translationmanager translationmanager.cpp translationmanager.h)
target_link_libraries(translationmanager PUBLIC Qt5::Core Qt5::Widgets shared)
target_include_directories(translationmanager PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
add_dependencies(translationmanager l10n)
0707010000002a000081a400000000000000000000000168cf694000000a9e000000000000000000000000000000000000002000000000src/i18n/translationmanager.cpp// system headers
#include <iostream>
// library headers
#include <QDebug>
#include <QDir>
#include <QLibraryInfo>
#include <QString>
// local headers
#include <shared.h>
#include "translationmanager.h"
TranslationManager::TranslationManager(QCoreApplication& app) : app(app) {
// set up translations
auto qtTranslator = new QTranslator();
qtTranslator->load("qt_" + QLocale::system().name(), QLibraryInfo::location(QLibraryInfo::TranslationsPath));
app.installTranslator(qtTranslator);
const auto systemLocale = QLocale::system().name();
// we're using primarily short names for translations, so we should load these translations as well
const auto shortSystemLocale = systemLocale.split('_')[0];
const auto translationDir = getTranslationDir();
auto myappTranslator = new QTranslator();
myappTranslator->load(translationDir + "/ui." + systemLocale + ".qm");
myappTranslator->load(translationDir + "/ui." + shortSystemLocale + ".qm");
app.installTranslator(myappTranslator);
// store translators in list so they won't be deleted
installedTranslators.push_back(qtTranslator);
installedTranslators.push_back(myappTranslator);
}
TranslationManager::~TranslationManager() {
for (auto& translator : installedTranslators) {
delete translator;
translator = nullptr;
}
}
QString TranslationManager::getTranslationDir() {
// first we need to find the translation directory
// if this is run from the build tree, we try a path that can only work within the build directory
// then, we try the expected install location relative to the main binary
const auto binaryDirPath = QApplication::applicationDirPath();
// previously the path to the repo root dir was embedded to allow for finding the compiled translations
// this lead to irreproducible builds
// therefore the files are now generated within the build dir, and we guess the path based on the binary location
auto translationDir = binaryDirPath + "/../../i18n/generated/l10n";
// when the application is installed, we need to look for the files in the private data directory
if (!QDir(translationDir).exists()) {
auto privateDataDir = pathToPrivateDataDirectory();
if (!privateDataDir.isEmpty()) {
translationDir = privateDataDir + "/l10n";
}
}
// give the user (and dev) some feedback whether the translations could actually be found or not
if (!QDir(translationDir).exists()) {
std::cerr << "[AppImageLauncher] Warning: "
<< "Translation directory could not be found, translations are likely not available" << std::endl;
}
return translationDir;
}
0707010000002b000081a400000000000000000000000168cf694000000223000000000000000000000000000000000000001e00000000src/i18n/translationmanager.h#pragma once
// library includes
#include <QApplication>
#include <QTranslator>
#include <QList>
/*
* Installs translations for AppImageLauncher UIs in a Qt application.
*
* You need to keep instances of this alive until the application is terminated.
*/
class TranslationManager {
private:
const QCoreApplication& app;
QList<QTranslator*> installedTranslators;
public:
explicit TranslationManager(QCoreApplication& app);
~TranslationManager();
public:
// get translation dir
static QString getTranslationDir();
};
0707010000002c000041ed00000000000000000000000168cf694000000000000000000000000000000000000000000000000900000000src/i18n0707010000002d000081a400000000000000000000000168cf6940000001f6000000000000000000000000000000000000001a00000000src/shared/CMakeLists.txtadd_library(shared STATIC shared.h shared.cpp types.h types.cpp)
target_link_libraries(shared PUBLIC PkgConfig::glib Qt5::Core Qt5::Widgets Qt5::DBus libappimage translationmanager trashbin)
if(ENABLE_UPDATE_HELPER)
target_link_libraries(shared PUBLIC libappimageupdate)
endif()
target_compile_definitions(shared
PRIVATE -DPRIVATE_LIBDIR="${_private_libdir}"
PRIVATE -DCMAKE_PROJECT_SOURCE_DIR="${PROJECT_SOURCE_DIR}"
)
target_include_directories(shared PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
0707010000002e000081a400000000000000000000000168cf69400000c0cb000000000000000000000000000000000000001600000000src/shared/shared.cpp// system includes
#include <fstream>
#include <iostream>
#include <memory>
#include <sstream>
#include <tuple>
extern "C" {
#include <appimage/appimage.h>
#include <glib.h>
// #include <libgen.h>
#include <sys/stat.h>
#include <stdio.h>
#include <unistd.h>
}
// library includes
#include <QDebug>
#include <QIcon>
#include <QtDBus>
#include <QDirIterator>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QLibraryInfo>
#include <QMap>
#include <QMapIterator>
#include <QMessageBox>
#include <QObject>
#include <QRegularExpression>
#include <QSet>
#include <QSettings>
#include <QStandardPaths>
#include <QWindow>
#include <QPushButton>
#include <QPixmap>
#ifdef ENABLE_UPDATE_HELPER
#include <appimage/update.h>
#endif
// local headers
#include "shared.h"
#include "translationmanager.h"
static void gKeyFileDeleter(GKeyFile* ptr) {
if (ptr != nullptr)
g_key_file_free(ptr);
}
static void gErrorDeleter(GError* ptr) {
if (ptr != nullptr)
g_error_free(ptr);
}
bool makeExecutable(const QString& path) {
struct stat fileStat{};
if (stat(path.toStdString().c_str(), &fileStat) != 0) {
std::cerr << "Failed to call stat() on " << path.toStdString() << std::endl;
return false;
}
// no action required when file is executable already
// this could happen in scenarios when an AppImage is in a read-only location
if ((fileStat.st_uid == getuid() && fileStat.st_mode & 0100) ||
(fileStat.st_gid == getgid() && fileStat.st_mode & 0010) ||
(fileStat.st_mode & 0001)) {
return true;
}
return chmod(path.toStdString().c_str(), fileStat.st_mode | 0111) == 0;
}
bool makeNonExecutable(const QString& path) {
struct stat fileStat{};
if (stat(path.toStdString().c_str(), &fileStat) != 0) {
std::cerr << "Failed to call stat() on " << path.toStdString() << std::endl;
return false;
}
auto permissions = fileStat.st_mode;
// remove executable permissions
for (const auto permPart : {0100, 0010, 0001}) {
if (permissions & permPart)
permissions -= permPart;
}
return chmod(path.toStdString().c_str(), permissions) == 0;
}
QString expandTilde(QString path) {
if ((path.size() == 1 && path[0] == '~') || (path.size() >= 2 && path.startsWith("~/"))) {
path.remove(0, 1);
path.prepend(QDir::homePath());
}
return path;
}
// calculate path to config file
QString getConfigFilePath() {
const auto configPath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation);
const auto configFilePath = configPath + "/appimagelauncher.cfg";
return configFilePath;
}
void createConfigFile(int askToMove,
const QString& destination,
int enableDaemon,
const QStringList& additionalDirsToWatch,
int monitorMountedFilesystems) {
auto configFilePath = getConfigFilePath();
QFile file(configFilePath);
file.open(QIODevice::WriteOnly);
// cannot use QSettings because it doesn't support comments
// let's do it manually and hope for the best
file.write("[AppImageLauncher]\n");
if (askToMove < 0) {
file.write("# ask_to_move = true\n");
} else {
file.write("ask_to_move = ");
if (askToMove == 0) {
file.write("false");
} else {
file.write("true");
}
file.write("\n");
}
if (destination.isEmpty()) {
file.write("# destination = ~/Applications\n");
} else {
file.write("destination = ");
file.write(destination.toUtf8());
file.write("\n");
}
if (enableDaemon < 0) {
file.write("# enable_daemon = true\n");
} else {
file.write("enable_daemon = ");
if (enableDaemon == 0) {
file.write("false");
} else {
file.write("true");
}
file.write("\n");
}
file.write("\n\n");
// daemon configs
file.write("[appimagelauncherd]\n");
if (additionalDirsToWatch.empty()) {
file.write("# additional_directories_to_watch = ~/otherApplications:/even/more/applications\n");
} else {
file.write("additional_directories_to_watch = ");
file.write(additionalDirsToWatch.join(':').toUtf8());
file.write("\n");
}
if (monitorMountedFilesystems < 0) {
file.write("# monitor_mounted_filesystems = false\n");
} else {
file.write("monitor_mounted_filesystems = ");
if (monitorMountedFilesystems == 0) {
file.write("false");
} else {
file.write("true");
}
file.write("\n");
}
}
QSettings* getConfig(QObject* parent) {
auto configFilePath = getConfigFilePath();
auto* settings = new QSettings(configFilePath, QSettings::IniFormat, parent);
// expand ~ in paths in the config file with $HOME
const auto keysContainingPath = {
"AppImageLauncher/destination",
};
for (const QString& keyContainingPath : keysContainingPath){
if (settings->contains(keyContainingPath)) {
auto newValue = expandTilde(settings->value(keyContainingPath).toString());
settings->setValue(keyContainingPath, newValue);
}
}
return settings;
}
// TODO: check if this works with Wayland
bool isHeadless() {
bool isHeadless = true;
// not really clean to abuse env vars as "global storage", but hey, it works
if (getenv("_FORCE_HEADLESS")) {
return true;
}
QProcess proc;
proc.setProgram("xhost");
proc.setStandardOutputFile(QProcess::nullDevice());
proc.setStandardErrorFile(QProcess::nullDevice());
proc.start();
proc.waitForFinished();
switch (proc.exitCode()) {
case 255: {
// program not found, using fallback method
isHeadless = (getenv("DISPLAY") == nullptr);
break;
}
case 0:
case 1:
isHeadless = proc.exitCode() == 1;
break;
default:
throw std::runtime_error("Headless detection failed: unexpected exit code from xhost");
}
return isHeadless;
}
// avoids code duplication, and works for both graphical and non-graphical environments
void displayMessageBox(const QString& title, const QString& message, const QMessageBox::Icon icon) {
if (isHeadless()) {
std::cerr << title.toStdString() << ": " << message.toStdString() << std::endl;
} else {
// little complex, can't use QMessageBox::{critical,warning,...} for the same reason as in main()
auto* mb = new QMessageBox(icon, title, message, QMessageBox::Ok, nullptr);
mb->show();
QApplication::exec();
}
}
void displayError(const QString& message) {
displayMessageBox(QObject::tr("Error"), message, QMessageBox::Critical);
}
void displayWarning(const QString& message) {
displayMessageBox(QObject::tr("Warning"), message, QMessageBox::Warning);
}
QDir integratedAppImagesDestination() {
auto config = getConfig();
if (config == nullptr)
return DEFAULT_INTEGRATION_DESTINATION;
static const QString keyName("AppImageLauncher/destination");
if (config->contains(keyName))
return config->value(keyName).toString();
return DEFAULT_INTEGRATION_DESTINATION;
}
class Mount {
private:
QString device;
QString mountPoint;
QString fsType;
QString mountOptions;
public:
Mount(QString device, QString mountPoint, QString fsType, QString mountOptions) :
device(std::move(device)),
mountPoint(std::move(mountPoint)),
fsType(std::move(fsType)),
mountOptions(std::move(
mountOptions)) {}
Mount(const Mount& other) = default;
Mount& operator=(const Mount& other) = default;
public:
const QString& getDevice() const {
return device;
}
const QString& getMountPoint() const {
return mountPoint;
}
const QString& getFsType() const {
return fsType;
}
const QString& getMountOptions() const {
return mountOptions;
}
};
QList<Mount> listMounts() {
QList<Mount> mountedDirectories;
std::ifstream ifs("/proc/mounts");
std::string _currentLine;
while (std::getline(ifs, _currentLine)) {
const auto currentLine = QString::fromStdString(_currentLine);
const auto parts = currentLine.split(" ");
mountedDirectories << Mount{parts[0], parts[1], parts[2], parts[3]};
}
return mountedDirectories;
}
QSet<QString> additionalAppImagesLocations(const bool includeAllMountPoints) {
QSet<QString> additionalLocations;
additionalLocations << "/Applications";
// integrate AppImages from mounted filesystems, if requested
// we don't want to read files from any FUSE mounted filesystems nor from any virtual filesystems
// to
static const auto validFilesystems = {"ext2", "ext3", "ext4", "ntfs", "vfat", "btrfs"};
static const auto blacklistedMountPointPrefixes = {
"/var/lib/schroot",
"/run/docker",
"/boot",
"/sys",
"/proc",
"/snap",
};
if (includeAllMountPoints) {
for (const auto& mount : listMounts()) {
const auto& device = mount.getDevice();
const auto& mountPoint = mount.getMountPoint();
const auto& fsType = mount.getFsType();
// we have to filter out virtual filesystems, i.e., ones which have a "nonsense" device path
// any device that doesn't start with / is likely virtual, this is the first indicator
if (device.size() < 1 || device[0] != '/') {
continue;
}
// the device should exist for obvious reasons
if (!QFileInfo(QFileInfo(device).absoluteFilePath()).exists()) {
continue;
}
// we don't want to mount any loop-mounted or bind-mounted or other devices, only... "native" ones
// therefore we permit only "real" devices listed within /dev
if (!device.startsWith("/dev/")) {
continue;
}
// there's a few locations which we know we don't want to search for AppImages in
// either it's a waste of time or otherwise a bad idea, but it will surely save time *not* to search them
if (std::find_if(blacklistedMountPointPrefixes.begin(), blacklistedMountPointPrefixes.end(),
[&mountPoint](const QString& prefix) {
return mountPoint == prefix || mountPoint.startsWith(prefix + "/");
}) != blacklistedMountPointPrefixes.end()) {
continue;
}
// we can skip the root mount point, as we handled it above
if (mountPoint == "/") {
continue;
}
// we only support a limited set of filesystems
if (std::find(validFilesystems.begin(), validFilesystems.end(), fsType) == validFilesystems.end()) {
continue;
}
// sanity check -- can likely be removed in the future
if (mountPoint.isEmpty()) {
const auto message = "empty mount point for mount with device " + device.toStdString();
throw std::invalid_argument(message);
}
// assemble potential applications location; caller needs to check whether the directory exists before setting
// up e.g., an inotify watch
const QString additionalLocation(mountPoint + "/Applications");
additionalLocations << additionalLocation;
}
}
return additionalLocations;
}
bool shallMonitorMountedFilesystems(const QSettings* config) {
Q_ASSERT(config != nullptr);
return config->value("appimagelauncherd/monitor_mounted_filesystems", "false").toBool();
}
QDirSet getAdditionalDirectoriesFromConfig(const QSettings* config) {
Q_ASSERT(config != nullptr);
constexpr auto configKey = "appimagelauncherd/additional_directories_to_watch";
const auto configValue = config->value(configKey, "").toString();
qDebug() << configKey << "value:" << configValue;
QDirSet additionalDirs{};
for (auto dirPath : configValue.split(":")) {
// empty values will, for some reason, be interpreted as "use the home directory"
// as we don't want to accidentally monitor the home directory, we need to skip those values
if (dirPath.isEmpty()) {
qDebug() << "skipping empty directory path";
continue;
}
// make sure to have full path
qDebug() << "path before tilde expansion:" << dirPath;
dirPath = expandTilde(dirPath);
qDebug() << "path after tilde expansion:" << dirPath;
// non-absolute paths which don't contain a tilde cannot be resolved safely, they likley depend on the cwd
// therefore, we need to ignore those
if (!QFileInfo(dirPath).isAbsolute()) {
std::cerr << "Warning: path " << dirPath.toStdString() << " can not be resolved, skipping" << std::endl;
continue;
}
const QDir dir(dirPath);
if (!dir.exists()) {
std::cerr << "Warning: could not find directory " << dirPath.toStdString() << ", skipping" << std::endl;
continue;
}
additionalDirs.insert(dir);
}
return additionalDirs;
}
QDirSet daemonDirectoriesToWatch(const QSettings* config) {
QDirSet watchedDirectories;
// of course we need to watch the main integration directory
const auto defaultDestination = integratedAppImagesDestination();
// make sure it exists, otherwise the daemon doesn't have anything to do
if (!defaultDestination.exists()) {
defaultDestination.mkdir(".");
}
watchedDirectories.insert(defaultDestination);
// however, there's likely additional ones to watch, like a system-wide Applications directory
{
bool monitorMountedFilesystems = shallMonitorMountedFilesystems(config);
const auto additionalDirs = additionalAppImagesLocations(monitorMountedFilesystems);
for (const auto& d : additionalDirs) {
watchedDirectories.insert(QDir(d).absolutePath());
}
}
// also, we should include additional directories from the config file
{
const auto configProvidedDirectories = getAdditionalDirectoriesFromConfig(config);
std::copy(
configProvidedDirectories.begin(), configProvidedDirectories.end(),
std::inserter(watchedDirectories, watchedDirectories.end())
);
}
return watchedDirectories;
}
QString buildPathToIntegratedAppImage(const QString& pathToAppImage) {
// if type 2 AppImage, we can build a "content-aware" filename
// see #7 for details
auto digest = getAppImageDigestMd5(pathToAppImage);
const QFileInfo appImageInfo(pathToAppImage);
QString baseName = appImageInfo.completeBaseName();
// if digest is available, append a separator
if (!digest.isEmpty()) {
const auto digestSuffix = "_" + digest;
// check whether digest is already contained in filename
if (!pathToAppImage.contains(digestSuffix))
baseName += "_" + digest;
}
auto fileName = baseName;
// must not use completeSuffix() in combination with completeBasename(), otherwise the final filename is composed
// incorrectly
if (!appImageInfo.suffix().isEmpty()) {
fileName += "." + appImageInfo.suffix();
}
return integratedAppImagesDestination().path() + "/" + fileName;
}
std::map<std::string, std::string> findCollisions(const QString& currentNameEntry) {
std::map<std::string, std::string> collisions{};
// default locations of desktop files on systems
const auto directories = {
QString("/usr/share/applications/"),
QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/applications/"
};
for (const auto& directory : directories) {
QDirIterator iterator(directory, QDirIterator::FollowSymlinks);
while (iterator.hasNext()) {
const auto filename = iterator.next();
if (!QFileInfo(filename).isFile() || !filename.endsWith(".desktop"))
continue;
std::shared_ptr<GKeyFile> desktopFile(g_key_file_new(), gKeyFileDeleter);
std::shared_ptr<GError*> error(nullptr, gErrorDeleter);
// if the key file parser can't load the file, it's most likely not a valid desktop file, so we just skip this file
if (!g_key_file_load_from_file(desktopFile.get(), filename.toStdString().c_str(), G_KEY_FILE_KEEP_TRANSLATIONS, error.get()))
continue;
auto* nameEntry = g_key_file_get_string(desktopFile.get(), G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_NAME, error.get());
// invalid desktop file, needs to be skipped
if (nameEntry == nullptr)
continue;
if (QString(nameEntry).trimmed().startsWith(currentNameEntry.trimmed())) {
collisions[filename.toStdString()] = nameEntry;
}
}
}
return collisions;
}
bool updateDesktopDatabaseAndIconCaches() {
const auto dataLocation = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
const std::map<std::string, std::string> commands = {
{"update-desktop-database", dataLocation.toStdString() + "/applications"},
{"gtk-update-icon-cache-3.0", dataLocation.toStdString() + "/icons/hicolor/ -t"},
{"gtk-update-icon-cache", dataLocation.toStdString() + "/icons/hicolor/ -t"},
{"xdg-desktop-menu", "forceupdate"},
{"update-mime-database", dataLocation.toStdString() + "/mime "},
{"update-icon-caches", dataLocation.toStdString() + "/icons/"},
};
for (const auto& command : commands) {
// only call if the command exists
if (system(("which " + command.first + " 2>&1 1>/dev/null").c_str()) == 0) {
// exit codes are not evaluated intentionally
system((command.first + " " + command.second).c_str());
}
}
return true;
}
std::shared_ptr<char> getOwnBinaryPath() {
auto path = std::shared_ptr<char>(realpath("/proc/self/exe", nullptr));
if (path == nullptr)
throw std::runtime_error("Could not detect path to own binary; something must be horribly broken");
return path;
}
#ifndef BUILD_LITE
QString privateLibDirPath(const QString& srcSubdirName) {
// PRIVATE_LIBDIR will be a relative path most likely
// therefore, we need to detect the install prefix based on our own binary path, and then calculate the path to
// the helper tools based on that
const QString ownBinaryDirPath = QFileInfo(getOwnBinaryPath().get()).dir().absolutePath();
const QString installPrefixPath = QFileInfo(ownBinaryDirPath).dir().absolutePath();
QString privateLibDirPath = installPrefixPath + "/" + PRIVATE_LIBDIR;
// the following lines make things work during development: here, the build dir path is inserted instead, which
// allows for testing with the latest changes
if (!QDir(privateLibDirPath).exists()) {
// this makes sure that when we're running from a local dev build, we end up in the right directory
// very important when running this code from the daemon, since it's not in the same directory as the helpers
privateLibDirPath = ownBinaryDirPath + "/../" + srcSubdirName;
}
// if there is no such directory like <prefix>/bin/../lib/... or the binary is not found there, there is a chance
// the binary is just next to this one (this is the case in the update/remove helpers)
// therefore we compare the binary directory path with PRIVATE_LIBDIR
if (!QDir(privateLibDirPath).exists()) {
if (privateLibDirPath.contains(PRIVATE_LIBDIR)) {
privateLibDirPath = ownBinaryDirPath;
}
}
return privateLibDirPath;
}
#endif
bool installDesktopFileAndIcons(const QString& pathToAppImage, bool resolveCollisions) {
if (appimage_register_in_system(pathToAppImage.toStdString().c_str(), false) != 0) {
displayError(QObject::tr("Failed to register AppImage in system via libappimage"));
return false;
}
const auto* desktopFilePath = appimage_registered_desktop_file_path(pathToAppImage.toStdString().c_str(), nullptr, false);
// sanity check -- if the file doesn't exist, the function returns NULL
if (desktopFilePath == nullptr) {
displayError(QObject::tr("Failed to find integrated desktop file"));
return false;
}
// check that file exists
if (!QFile(desktopFilePath).exists()) {
displayError(QObject::tr("Couldn't find integrated AppImage's desktop file"));
return false;
}
/* write AppImageLauncher specific entries to desktop file
*
* unfortunately, QSettings doesn't work as a desktop file reader/writer, and libqtxdg isn't really meant to be
* used by projects via add_subdirectory/ExternalProject
* a system dependency is not an option for this project, and we link to glib already anyway, so let's just use
* glib, which is known to work
*/
std::shared_ptr<GKeyFile> desktopFile(g_key_file_new(), gKeyFileDeleter);
std::shared_ptr<GError*> error(nullptr, gErrorDeleter);
const auto flags = GKeyFileFlags(G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS);
auto handleError = [error, desktopFile]() {
std::ostringstream ss;
ss << QObject::tr("Failed to load desktop file:").toStdString() << std::endl << (*error)->message;
displayError(QString::fromStdString(ss.str()));
};
if (!g_key_file_load_from_file(desktopFile.get(), desktopFilePath, flags, error.get())) {
handleError();
return false;
}
const auto* nameEntry = g_key_file_get_string(desktopFile.get(), G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_NAME, error.get());
if (nameEntry == nullptr) {
displayWarning(QObject::tr("AppImage has invalid desktop file"));
}
if (resolveCollisions) {
// TODO: support multilingual collisions
auto collisions = findCollisions(nameEntry);
// make sure to remove own entry
collisions.erase(collisions.find(desktopFilePath));
if (!collisions.empty()) {
// collisions are resolved like in the filesystem: a monotonically increasing number in brackets is
// appended to the Name in order to keep the number monotonically increasing, we look for the highest
// number in brackets in the existing entries, add 1 to it, and append it in brackets to the current
// desktop file's Name entry
unsigned int currentNumber = 1;
QRegularExpression regex(R"(^.*\(([0-9]+)\)$)");
for (const auto& collision : collisions) {
const auto& currentNameEntry = collision.second;
auto match = regex.match(QString::fromStdString(currentNameEntry));
if (match.hasMatch()) {
// 0 = entire string
// 1 = first group
const QString numString = match.captured(1);
const int num = numString.toInt();
// monotonic counting, i.e., never try to "be smart" by e.g., filling in the gaps between
// previous numbers
if (num >= currentNumber) {
currentNumber = num + 1;
}
}
}
auto newName = QString(nameEntry) + " (" + QString::number(currentNumber) + ")";
g_key_file_set_string(desktopFile.get(), G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_NAME, newName.toStdString().c_str());
}
}
auto convertToCharPointerList = [](const std::vector<std::string>& stringList) {
std::vector<const char*> pointerList;
// reserve space to increase efficiency
pointerList.reserve(stringList.size());
// convert string list to list of const char pointers
for (const auto& action : stringList) {
pointerList.push_back(action.c_str());
}
return pointerList;
};
std::vector<std::string> desktopActions;
// we may not just overwrite the existing actions key, as then the actions cannot be used any more from the context menu
{
const auto* actionsEntry = g_key_file_get_string(desktopFile.get(), G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_ACTIONS, error.get());
for (const QString& action : QString(actionsEntry).split(";")) {
if (action.isEmpty()) {
continue;
}
desktopActions.emplace_back(action.toStdString());
}
}
// use a "vendor prefix" to avoid collisions with existing actions, as "Update" and "Remove" are generic terms
static const std::string removeActionKey{"AppImageLauncher-Remove-AppImage"};
static const std::string updateActionKey{"AppImageLauncher-Update-AppImage"};
desktopActions.emplace_back(removeActionKey);
// load translations from JSON file(s)
QMap<QString, QString> removeActionNameTranslations;
#ifdef ENABLE_UPDATE_HELPER
QMap<QString, QString> updateActionNameTranslations;
{
QDirIterator i18nDirIterator(TranslationManager::getTranslationDir());
while(i18nDirIterator.hasNext()) {
const auto& filePath = i18nDirIterator.next();
const auto& fileName = QFileInfo(filePath).fileName();
if (!QFileInfo(filePath).isFile() || !(fileName.startsWith("desktopfiles.") && fileName.endsWith(".json")))
continue;
// check whether filename's format is alright, otherwise parsing the locale might try to access a
// non-existing (or the wrong) member
auto splitFilename = fileName.split(".");
if (splitFilename.size() != 3)
continue;
// parse locale from filename
auto locale = splitFilename[1];
QFile jsonFile(filePath);
if (!jsonFile.open(QIODevice::ReadOnly)) {
displayWarning(QMessageBox::tr("Could not parse desktop file translations:\nCould not open file for reading:\n\n%1").arg(fileName));
}
// TODO: need to make sure that this doesn't try to read huge files at once
auto data = jsonFile.readAll();
QJsonParseError parseError{};
auto jsonDoc = QJsonDocument::fromJson(data, &parseError);
// show warning on syntax errors and continue
if (parseError.error != QJsonParseError::NoError || jsonDoc.isNull() || !jsonDoc.isObject()) {
displayWarning(QMessageBox::tr("Could not parse desktop file translations:\nInvalid syntax:\n\n%1").arg(parseError.errorString()));
}
auto jsonObj = jsonDoc.object();
for (const auto& key : jsonObj.keys()) {
auto value = jsonObj[key].toString();
auto splitKey = key.split("/");
if (key.startsWith("Desktop Action update")) {
qDebug() << "update: adding" << value << "for locale" << locale;
updateActionNameTranslations[locale] = value;
} else if (key.startsWith("Desktop Action remove")) {
qDebug() << "remove: adding" << value << "for locale" << locale;
removeActionNameTranslations[locale] = value;
}
}
}
}
#endif
#ifndef BUILD_LITE
auto privateLibDir = privateLibDirPath("ui");
const char helperIconName[] = "AppImageLauncher";
#else
const char helperIconName[] = "AppImageLauncher-Lite";
#endif
// add Remove action
{
const auto removeSectionName = "Desktop Action " + removeActionKey;
g_key_file_set_string(desktopFile.get(), removeSectionName.c_str(), "Name", "Delete this AppImage");
g_key_file_set_string(desktopFile.get(), removeSectionName.c_str(), "Icon", helperIconName);
std::ostringstream removeExecPath;
#ifndef BUILD_LITE
removeExecPath << privateLibDir.toStdString() << "/remove";
#else
removeExecPath << getenv("HOME") << "/.local/lib/appimagelauncher-lite/appimagelauncher-lite.AppImage remove";
#endif
removeExecPath << " \"" << pathToAppImage.toStdString() << "\"";
g_key_file_set_string(desktopFile.get(), removeSectionName.c_str(), "Exec", removeExecPath.str().c_str());
// install translations
auto it = QMapIterator<QString, QString>(removeActionNameTranslations);
while (it.hasNext()) {
auto entry = it.next();
g_key_file_set_locale_string(desktopFile.get(), removeSectionName.c_str(), "Name", entry.key().toStdString().c_str(), entry.value().toStdString().c_str());
}
}
#ifdef ENABLE_UPDATE_HELPER
// add Update action
{
appimage::update::Updater updater(pathToAppImage.toStdString());
// but only if there's update information
if (!updater.updateInformation().empty()) {
// section needs to be announced in desktop actions list
desktopActions.emplace_back(updateActionKey);
const auto updateSectionName = "Desktop Action " + updateActionKey;
g_key_file_set_string(desktopFile.get(), updateSectionName.c_str(), "Name", "Update this AppImage");
g_key_file_set_string(desktopFile.get(), updateSectionName.c_str(), "Icon", helperIconName);
std::ostringstream updateExecPath;
#ifndef BUILD_LITE
updateExecPath << privateLibDir.toStdString() << "/update";
#else
updateExecPath << getenv("HOME") << "/.local/lib/appimagelauncher-lite/appimagelauncher-lite.AppImage update";
#endif
updateExecPath << " \"" << pathToAppImage.toStdString() << "\"";
g_key_file_set_string(desktopFile.get(), updateSectionName.c_str(), "Exec", updateExecPath.str().c_str());
// install translations
auto it = QMapIterator<QString, QString>(updateActionNameTranslations);
while (it.hasNext()) {
auto entry = it.next();
g_key_file_set_locale_string(desktopFile.get(), updateSectionName.c_str(), "Name", entry.key().toStdString().c_str(), entry.value().toStdString().c_str());
}
}
}
#endif
// add desktop actions key
g_key_file_set_string_list(
desktopFile.get(),
G_KEY_FILE_DESKTOP_GROUP,
G_KEY_FILE_DESKTOP_KEY_ACTIONS,
convertToCharPointerList(desktopActions).data(),
desktopActions.size()
);
// add version key
const auto version = QApplication::applicationVersion().replace("version ", "").toStdString();
g_key_file_set_string(desktopFile.get(), G_KEY_FILE_DESKTOP_GROUP, "X-AppImageLauncher-Version", version.c_str());
// save desktop file to disk
if (!g_key_file_save_to_file(desktopFile.get(), desktopFilePath, error.get())) {
handleError();
return false;
}
// make desktop file executable ("trustworthy" to some DEs)
// TODO: handle this in libappimage
makeExecutable(desktopFilePath);
// notify KDE/Plasma about icon change
{
auto message = QDBusMessage::createSignal(QStringLiteral("/KIconLoader"), QStringLiteral("org.kde.KIconLoader"), QStringLiteral("iconChanged"));
message.setArguments({0});
QDBusConnection::sessionBus().send(message);
}
return true;
}
bool updateDesktopFileAndIcons(const QString& pathToAppImage) {
return installDesktopFileAndIcons(pathToAppImage, true);
}
IntegrationState integrateAppImage(const QString& pathToAppImage, const QString& pathToIntegratedAppImage) {
// need std::strings to get working pointers with .c_str()
const auto oldPath = pathToAppImage.toStdString();
const auto newPath = pathToIntegratedAppImage.toStdString();
// create target directory
QDir().mkdir(QFileInfo(QFile(pathToIntegratedAppImage)).dir().absolutePath());
// check whether AppImage is in integration directory already
if (QFileInfo(pathToAppImage).absoluteFilePath() != QFileInfo(pathToIntegratedAppImage).absoluteFilePath()) {
// need to check whether file exists
// if it does, the existing AppImage needs to be removed before rename can be called
if (QFile(pathToIntegratedAppImage).exists()) {
std::ostringstream message;
message << QObject::tr("AppImage with same filename has already been integrated.").toStdString() << std::endl
<< std::endl
<< QObject::tr("Do you wish to overwrite the existing AppImage?").toStdString() << std::endl
<< QObject::tr("Choosing No will run the AppImage once, and leave the system in its current state.").toStdString();
auto* messageBox = new QMessageBox(
QMessageBox::Warning,
QObject::tr("Warning"),
QString::fromStdString(message.str()),
QMessageBox::Yes | QMessageBox::No
);
messageBox->setDefaultButton(QMessageBox::No);
messageBox->show();
QApplication::exec();
if (messageBox->clickedButton() == messageBox->button(QMessageBox::No)) {
return INTEGRATION_ABORTED;
}
QFile(pathToIntegratedAppImage).remove();
}
if (!QFile(pathToAppImage).rename(pathToIntegratedAppImage)) {
auto* messageBox = new QMessageBox(
QMessageBox::Critical,
QObject::tr("Error"),
QObject::tr("Failed to move AppImage to target location.\n"
"Try to copy AppImage instead?"),
QMessageBox::Ok | QMessageBox::Cancel
);
messageBox->setDefaultButton(QMessageBox::Ok);
messageBox->show();
QApplication::exec();
if (messageBox->clickedButton() == messageBox->button(QMessageBox::Cancel))
return INTEGRATION_FAILED;
if (!QFile(pathToAppImage).copy(pathToIntegratedAppImage)) {
displayError("Failed to copy AppImage to target location");
return INTEGRATION_FAILED;
}
}
}
if (!installDesktopFileAndIcons(pathToIntegratedAppImage))
return INTEGRATION_FAILED;
return INTEGRATION_SUCCESSFUL;
}
QString getAppImageDigestMd5(const QString& path) {
// try to read embedded MD5 digest
unsigned long offset = 0, length = 0;
// first of all, digest calculation is supported only for type 2
if (appimage_get_type(path.toStdString().c_str(), false) != 2)
return "";
auto rv = appimage_get_elf_section_offset_and_length(path.toStdString().c_str(), ".digest_md5", &offset, &length);
QByteArray buffer(16, '\0');
if (rv && offset != 0 && length != 0) {
// open file and read digest from ELF header section
QFile file(path);
if (!file.open(QFile::ReadOnly))
return "";
if (!file.seek(static_cast<qint64>(offset)))
return "";
if (!file.read(buffer.data(), buffer.size()))
return "";
file.close();
}
bool needToCalculateDigest;
// there seem to be some AppImages out there who actually have the required section embedded, but it's empty
// therefore we make the assumption that a hash value of zeroes is probably incorrect and recalculate
// in the extremely rare case in which the AppImage's digest would *really* be that value, we'd waste a bit of
// computation time, but the chances are so low... who cares, right?
{
auto nonZeroCharacterFound = false;
for (const char i : buffer) {
if (i != '\0') {
nonZeroCharacterFound = true;
break;
}
}
needToCalculateDigest = !nonZeroCharacterFound;
}
if (needToCalculateDigest) {
// calculate digest
if (!appimage_type2_digest_md5(path.toStdString().c_str(), buffer.data()))
return "";
}
// create hexadecimal representation
auto hexDigest = appimage_hexlify(buffer, static_cast<size_t>(buffer.size()));
QString hexDigestStr(hexDigest);
free(hexDigest);
return hexDigestStr;
}
bool hasAlreadyBeenIntegrated(const QString& pathToAppImage) {
return appimage_is_registered_in_system(pathToAppImage.toStdString().c_str());
}
bool isInDirectory(const QString& pathToAppImage, const QDir& directory) {
return directory == QFileInfo(pathToAppImage).absoluteDir();
}
bool cleanUpOldDesktopIntegrationResources(bool verbose) {
auto dirPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/applications";
auto directory = QDir(dirPath);
QStringList filters;
filters << "appimagekit_*.desktop";
directory.setNameFilters(filters);
for (auto desktopFilePath : directory.entryList()) {
desktopFilePath = dirPath + "/" + desktopFilePath;
std::shared_ptr<GKeyFile> desktopFile(g_key_file_new(), [](GKeyFile* p) {
g_key_file_free(p);
});
if (!g_key_file_load_from_file(desktopFile.get(), desktopFilePath.toStdString().c_str(), G_KEY_FILE_NONE, nullptr)) {
continue;
}
std::shared_ptr<char> execValue(g_key_file_get_string(desktopFile.get(), G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_EXEC, nullptr), [](char* p) {
free(p);
});
// if there is no Exec value in the file, the desktop file is apparently broken, therefore we skip the file
if (execValue == nullptr) {
continue;
}
std::shared_ptr<char> tryExecValue(g_key_file_get_string(desktopFile.get(), G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_TRY_EXEC, nullptr), [](char* p) {
free(p);
});
// TryExec is optional, although recently the desktop integration functions started to force add such keys
// with a path to the desktop file
// (before, if it existed, the key was replaced with the AppImage's path)
// If it exists, we assume its value is the full path to the AppImage, which can be used to check the existence
// of the AppImage
QString appImagePath;
if (tryExecValue != nullptr) {
appImagePath = QString(tryExecValue.get());
} else {
appImagePath = QString(execValue.get()).split(" ").first();
}
// now, check whether AppImage exists
// FIXME: the split command for the Exec value might not work if there's a space in the filename
// we really need a parser that understands the desktop file escaping
if (!QFile(appImagePath).exists()) {
if (verbose)
std::cout << "AppImage no longer exists, cleaning up resources: " << appImagePath.toStdString() << std::endl;
if (verbose)
std::cout << "Removing desktop file: " << desktopFilePath.toStdString() << std::endl;
QFile(desktopFilePath).remove();
// TODO: clean up related resources such as icons or MIME definitions
auto* iconValue = g_key_file_get_string(desktopFile.get(), G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_ICON, nullptr);
if (iconValue != nullptr) {
const auto dataLocation = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
const auto iconsPath = QString::fromStdString(dataLocation.toStdString() + "/share/icons/");
for (QDirIterator it(iconsPath, QDirIterator::Subdirectories); it.hasNext();) {
auto path = it.next();
if (QFileInfo(path).completeBaseName().startsWith(iconValue)) {
QFile::remove(path);
}
}
}
}
}
return true;
}
time_t getMTime(const QString& path) {
struct stat st{};
if (stat(path.toStdString().c_str(), &st) != 0) {
displayError(QObject::tr("Failed to call stat() on path:\n\n%1").arg(path));
return -1;
}
return st.st_mtim.tv_sec;
}
bool desktopFileHasBeenUpdatedSinceLastUpdate(const QString& pathToAppImage) {
const auto ownBinaryPath = getOwnBinaryPath();
const auto desktopFilePath = appimage_registered_desktop_file_path(pathToAppImage.toStdString().c_str(), nullptr, false);
auto ownBinaryMTime = getMTime(ownBinaryPath.get());
auto desktopFileMTime = getMTime(desktopFilePath);
// check if something has failed horribly
if (desktopFileMTime < 0 || ownBinaryMTime < 0)
return false;
return desktopFileMTime > ownBinaryMTime;
}
bool isAppImage(const QString& path) {
const auto type = appimage_get_type(path.toUtf8(), false);
return type > 0 && type <= 2;
}
QString which(const std::string& name) {
std::vector<char> command(4096);
snprintf(command.data(), command.size()-1, "which %s", name.c_str());
auto* proc = popen(command.data(), "r");
if (proc == nullptr)
throw std::runtime_error("Failed to start process for which");
std::vector<char> outBuf(4096);
fread(outBuf.data(), sizeof(char), outBuf.size()-1, proc);
pclose(proc);
QString rv(outBuf.data());
rv.replace("\n", "");
return rv;
}
void checkAuthorizationAndShowDialogIfNecessary(const QString& path, const QString& question) {
const uint32_t ownUid = getuid();
const uint32_t fileOwnerUid = QFileInfo(path).ownerId();
const auto fileOwnerUsername = QFileInfo(path).owner();
if (ownUid != fileOwnerUid) {
qDebug() << "attempting relaunch with root helper";
QString messageBoxText = QMessageBox::tr("File %1 is owned by another user: %2").arg(path).arg(fileOwnerUsername);
messageBoxText += "\n\n";
messageBoxText += question;
auto* messageBox = new QMessageBox(
QMessageBox::Warning,
QMessageBox::tr("Permissions problem"),
messageBoxText,
QMessageBox::Ok | QMessageBox::Abort,
nullptr
);
messageBox->setDefaultButton(QMessageBox::Ok);
messageBox->show();
QApplication::exec();
const auto relaunch = messageBox->clickedButton() == messageBox->button(QMessageBox::Ok);
if (!relaunch) {
qDebug() << "Dialog aborted";
exit(1);
}
qDebug() << "ok, attempting relaunch with root helper";
// pkexec doesn't retain $DISPLAY etc., as per the man page, so we can't run UI programs with it
for (const auto& rootHelperFilename : {/*"pkexec",*/ "gksudo", "gksu"}) {
const auto rootHelperPath = which(rootHelperFilename);
qDebug() << "trying root helper " << rootHelperFilename << rootHelperPath;
if (rootHelperPath.isEmpty())
continue;
qDebug() << rootHelperFilename << rootHelperPath;
std::vector<char*> argv = {
strdup(rootHelperPath.toStdString().c_str()),
};
if (fileOwnerUid != 0) {
argv.emplace_back(strdup("--user"));
argv.emplace_back(strdup(std::to_string(fileOwnerUid).c_str()));
}
for (const auto& arg : QCoreApplication::arguments()) {
argv.emplace_back(strdup(arg.toStdString().c_str()));
}
argv.emplace_back(nullptr);
const auto rv = execv(strdup(rootHelperPath.toStdString().c_str()), argv.data());
// if the execution fails, we should signalize this to the user instead of silently failing over to the
// next tool
QMessageBox::critical(
nullptr,
QMessageBox::tr("Error"),
QMessageBox::tr("Failed to run permissions helper, exited with return code %1").arg(rv)
);
exit(1);
}
QMessageBox::critical(
nullptr,
QMessageBox::tr("Error"),
QMessageBox::tr("Could not find suitable permissions helper, aborting")
);
exit(1);
}
}
QString pathToPrivateDataDirectory() {
// first we need to find the translation directory
// if this is run from the build tree, we try a path that can only work within the build directory
// then, we try the expected install location relative to the main binary
const auto binaryDirPath = QApplication::applicationDirPath();
// our helper tools are not shipped in usr/bin but usr/lib/<arch>-linux-gnu/appimagelauncher
// therefore we need to check for the translations directory relative to this directory as well
// as <arch-linux-gnu> may not be used in the path, we also check for its parent directory
QString dataDir = binaryDirPath + "/../../share/appimagelauncher/";
if (!QDir(dataDir).exists()) {
dataDir = binaryDirPath + "/../../../share/appimagelauncher/";
}
// this directory should work for the main application in usr/bin
if (!QDir(dataDir).exists()) {
dataDir = binaryDirPath + "/../share/appimagelauncher/";
}
if (!QDir(dataDir).exists()) {
std::cerr << "[AppImageLauncher] Warning: "
<< "Path to private data directory could not be found" << std::endl;
return "";
}
return dataDir;
}
bool unregisterAppImage(const QString& pathToAppImage) {
auto rv = appimage_unregister_in_system(pathToAppImage.toStdString().c_str(), false);
if (rv != 0)
return false;
return true;
}
QIcon loadIconWithFallback(const QString& iconName) {
const QString subdirName("fallback-icons");
const auto binaryDir = QApplication::applicationDirPath();
// first we check the directory that would be expected with in the build environment
QDir fallbackIconDirectory = QDir(binaryDir + "/../../resources/" + subdirName);
// if that doesn't work, we check the private data directory, which should work when AppImageLauncher is installed
// through the packages or in Lite's AppImage
if (!fallbackIconDirectory.exists()) {
auto privateDataDir = pathToPrivateDataDirectory();
if (privateDataDir.length() > 0 && QDir(privateDataDir).exists()) {
fallbackIconDirectory = QDir(pathToPrivateDataDirectory() + "/" + subdirName);
}
}
// fallback icons aren't critical enough to exit the application if they can't be found
// after all, the theme icons may work just as well
if (!fallbackIconDirectory.exists()) {
std::cerr << "[AppImageLauncher] Warning:"
<< "fallback icons could not be loaded: directory could not be found" << std::endl;
return QIcon{};
}
qDebug() << "Loading fallback for icon" << iconName;
const auto iconFilename = iconName + ".svg";
const auto iconPath = fallbackIconDirectory.filePath(iconFilename);
if (!QFileInfo(iconPath).isFile()) {
std::cerr << "[AppImageLauncher] Warning: can't find fallback icon for name"
<< iconName.toStdString() << std::endl;
return QIcon{};
}
const auto fallbackIcon = QIcon(iconPath);
qDebug() << fallbackIcon;
return fallbackIcon;
}
void setUpFallbackIconPaths(QWidget* parent) {
/**
* Qt 5.12 adds a feature to add fallback paths for icons. This is a very simple way to automatically load custom
* icons when the icon theme doesn't provide a suitable alternative.
* However, we need to support a much older Qt version. Therefore we cannot use this very very handy feature.
* We basically iterate over all buttons which carry an icon and (re)load it, but this time provide a fallback
* loaded from our private data directory.
*/
// for now we only support buttons
// we could always add more widgets which provide an icon property
const auto buttons = parent->findChildren<QAbstractButton*>();
for (const auto& button : buttons) {
const auto iconName = button->icon().name();
// sort out buttons without an icon
if (iconName.length() <= 0)
continue;
// load icon from theme, providing the bundled icon as a fallback
// loading an "empty" (i.e., isNull() returns true) icon as fallback, as returned by loadIconWithFallback(...),
// works just fine
auto fallbackIcon = loadIconWithFallback(iconName);
auto newIcon = QIcon::fromTheme(iconName, fallbackIcon);
if (newIcon.isNull() || newIcon.pixmap(16, 16).isNull())
newIcon = fallbackIcon;
// now replace the button's actual icon with the fallback-enabled one
button->setIcon(newIcon);
}
}
0707010000002f000081a400000000000000000000000168cf69400000162f000000000000000000000000000000000000001400000000src/shared/shared.h/* central file for utility functions */
#pragma once
// system headers
#include <string>
#include <memory>
// library headers
#include <QDir>
#include <QString>
#include <QSettings>
// local headers
#include "types.h"
enum IntegrationState {
INTEGRATION_FAILED = 0,
INTEGRATION_SUCCESSFUL,
INTEGRATION_ABORTED
};
// standard location for integrated AppImages
// currently hardcoded, can not be changed by users
static const auto DEFAULT_INTEGRATION_DESTINATION = QString(getenv("HOME")) + "/Applications/";
// little convenience method to display warnings
void displayWarning(const QString& message);
// little convenience method to display errors
void displayError(const QString& message);
// reliable way to check if the current session is graphical or not
bool isHeadless();
// makes an existing file executable
bool makeExecutable(const QString& path);
// removes executable bits from file's permissions
bool makeNonExecutable(const QString& path);
#ifndef BUILD_LITE
// calculate path to private libdir, containing tools and libraries specific to and used by AppImageLauncher
QString privateLibDirPath(const QString& srcSubdirName);
#endif
// installs desktop file for given AppImage, including AppImageLauncher specific modifications
// set resolveCollisions to false in order to leave the Name entries as-is
bool installDesktopFileAndIcons(const QString& pathToAppImage, bool resolveCollisions = true);
// update AppImage's existing desktop file with AppImageLauncher specific entries
// this alias for installDesktopFileAndIcons does not perform any collision detection and resolving
bool updateDesktopFileAndIcons(const QString& pathToAppImage);
// update desktop database and icon caches of desktop environments
// this makes sure that:
// - outdated entries are removed from the launcher
// - icons of freshly integrated AppImages are displayed in the launcher
bool updateDesktopDatabaseAndIconCaches();
// integrates an AppImage using a standard workflow used across all AppImageLauncher applications
IntegrationState integrateAppImage(const QString& pathToAppImage, const QString& pathToIntegratedAppImage);
// write config file to standard location with given configuration values
// askToMove and enableDaemon both are bools but represented as int to add some sort of "unset" state
// < 0: unset; 0 = false; > 0 = true
// destination is a string that, when empty, will be interpreted as "use default"
void createConfigFile(int askToMove, const QString& destination, int enableDaemon,
const QStringList& additionalDirsToWatch = {}, int monitorMountedFilesystems = -1);
// replaces ~ character in paths with real home directory, if necessary and possible
QString expandTilde(QString path);
// load config file and return it
QSettings* getConfig(QObject* parent = nullptr);
// return directory into which the integrated AppImages will be moved
QDir integratedAppImagesDestination();
// additional directories to monitor for AppImages, and to permit AppImages to be within (i.e., shall not ask whether
// to move to the main location, if they're in one of these, it's all good)
QSet<QString> additionalAppImagesLocations(bool includeValidMountPoints = false);
// calculate list of directories the daemon has to watch
// AppImages inside there should furthermore not be moved out of there and into the main integration directory
QDirSet daemonDirectoriesToWatch(const QSettings* config);
// build path to standard location for integrated AppImages
QString buildPathToIntegratedAppImage(const QString& pathToAppImage);
// get AppImage MD5 digest
// extracts the digest embedded in the file
// if no such digest has been embedded, it calculates it using libappimage
QString getAppImageDigestMd5(const QString& path);
// checks whether AppImage has been integrated already
bool hasAlreadyBeenIntegrated(const QString& pathToAppImage);
// checks whether file is in a given directory
bool isInDirectory(const QString& pathToAppImage, const QDir& directory);
// clean up old desktop files (and related resources, such as icons)
bool cleanUpOldDesktopIntegrationResources(bool verbose = false);
// returns absolute path to currently running binary
std::shared_ptr<char> getOwnBinaryPath();
// returns true if AppImageLauncher was updated since the desktop file for a given AppImage has been updated last
bool desktopFileHasBeenUpdatedSinceLastUpdate(const QString& pathToAppImage);
// checks whether a file is an AppImage
bool isAppImage(const QString& path);
// when a file doesn't belong to the current user, this method shows a dialog asking whether to relaunch as that user
// this can be used when e.g., updating AppImages owned by root or other users
// uses pkexec, gksudo, gksu etc., whatever is available
// the second argument is the question that will be asked in the dialog displayed in case a relaunch is necessary
void checkAuthorizationAndShowDialogIfNecessary(const QString& path, const QString& question);
// searchs for path to private data directory relative to the current binary's location
// returns empty string if the path cannot be found
QString pathToPrivateDataDirectory();
// clean up desktop integration files installed while originally integrating the AppImage
bool unregisterAppImage(const QString& pathToAppImage);
// try to load icon with provided name from AppImageLauncher's fallback icons directory
// returns empty QIcon if such an icon cannot be found
// you can check for errors by calling QIcon::isNull()
QIcon loadIconWithFallback(const QString& iconName);
// sets up paths to fallback icons bundled with AppImageLauncher
void setUpFallbackIconPaths(QWidget*);
07070100000030000081a400000000000000000000000168cf6940000000c1000000000000000000000000000000000000001500000000src/shared/types.cpp#include "types.h"
QDebug operator<<(QDebug debug, const QDirSet& set) {
QDebugStateSaver saver(debug);
for (const auto& item : set) {
debug << item;
}
return debug;
}
07070100000031000081a400000000000000000000000168cf694000000167000000000000000000000000000000000000001300000000src/shared/types.h#pragma once
// system headers
#include <set>
// library headers
#include <QDebug>
#include <QDir>
struct QDirComparator {
public:
size_t operator()(const QDir& a, const QDir& b) const {
return a.absolutePath() < b.absolutePath();
}
};
typedef std::set<QDir, QDirComparator> QDirSet;
QDebug operator<<(QDebug debug, const QDirSet &set);
07070100000032000041ed00000000000000000000000168cf694000000000000000000000000000000000000000000000000b00000000src/shared07070100000033000081a400000000000000000000000168cf6940000000cb000000000000000000000000000000000000001c00000000src/trashbin/CMakeLists.txtadd_library(trashbin STATIC trashbin.cpp trashbin.h)
target_link_libraries(trashbin PUBLIC Qt5::Core libappimage shared)
target_include_directories(translationmanager PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
07070100000034000081a400000000000000000000000168cf694000000a91000000000000000000000000000000000000001a00000000src/trashbin/trashbin.cpp// system includes
#include <iostream>
#include <sys/stat.h>
// library includes
#include <QDateTime>
#include <QDir>
#include <QDirIterator>
#include <appimage/appimage.h>
// local includes
#include "trashbin.h"
#include "shared.h"
class TrashBin::PrivateData {
public:
const QDir dir;
public:
PrivateData() : dir(integratedAppImagesDestination().path() + "/.trash") {
// make sure trash directory exists
QDir(integratedAppImagesDestination().path()).mkdir(".trash");
}
bool canBeCleanedUp(const QString& path) {
return true;
}
};
TrashBin::TrashBin() {
d = new PrivateData();
}
QString TrashBin::path() {
return d->dir.path();
}
bool TrashBin::disposeAppImage(const QString& pathToAppImage) {
if (!QFile(pathToAppImage).exists()) {
std::cerr << "No such file or directory: " << pathToAppImage.toStdString() << std::endl;
return false;
}
// moving AppImages into the trash bin might fail if there's a file with the same filename
// removing multiple files with the same filenames is a valid use case, though
// therefore, a timestamp shall be prepended to the filename
// it is very unlike that some user will remove more than a AppImage per second, but if that should be the case,
// we could eventually increase the precision of the timestamp
// for now, this is not necessary
auto timestamp = QDateTime::currentDateTime().toString(Qt::ISODate);
auto newPath = d->dir.path() + QString("/") + timestamp + "_" + QFileInfo(pathToAppImage).fileName();
if (!QFile(pathToAppImage).rename(newPath))
return false;
if (!makeNonExecutable(newPath))
return false;
return true;
}
bool TrashBin::cleanUp() {
for (QDirIterator iterator(d->dir, QDirIterator::FollowSymlinks); iterator.hasNext();) {
auto currentPath = iterator.next();
if (!QFileInfo(currentPath).isFile())
continue;
if (appimage_get_type(currentPath.toStdString().c_str(), false) <= 0)
continue;
if (!d->canBeCleanedUp(currentPath)) {
std::cerr << "Cannot clean up AppImage yet: " << currentPath.toStdString() << std::endl;
continue;
}
std::cerr << "Removing AppImage: " << currentPath.toStdString() << std::endl;
// silently ignore if files can not be removed
// they shall be removed on subsequent runs
// if this won't happen and the trash directory will only get bigger at some point, we might need to
// reconsider this decision
if (!QFile(currentPath).remove())
continue;
}
return true;
}
07070100000035000081a400000000000000000000000168cf694000000236000000000000000000000000000000000000001800000000src/trashbin/trashbin.h// system includes
#include <QString>
#pragma once
class TrashBin {
private:
class PrivateData;
PrivateData* d;
public:
TrashBin();
public:
QString path();
public:
// move AppImage into trash bin directory
bool disposeAppImage(const QString& pathToAppImage);
// check all AppImages in trash bin whether they can be removed
// this function should be called regularly to make sure the files in the trash bin are cleaned up as soon
// as possible
bool cleanUp();
};
07070100000036000041ed00000000000000000000000168cf694000000000000000000000000000000000000000000000000d00000000src/trashbin07070100000037000081a400000000000000000000000168cf6940000009a7000000000000000000000000000000000000001600000000src/ui/CMakeLists.txtif(NOT BUILD_LITE)
# main AppImageLauncher application
add_executable(AppImageLauncher main.cpp resources.qrc first-run.cpp first-run.h first-run.ui integration_dialog.cpp integration_dialog.h integration_dialog.ui)
target_link_libraries(AppImageLauncher shared PkgConfig::glib libappimage shared)
# set binary runtime rpath to make sure the libappimage.so built and installed by this project is going to be used
# by the installed binaries (be it the .deb, the AppImage, or whatever)
# in order to make the whole install tree relocatable, a relative path is used
set_target_properties(AppImageLauncher PROPERTIES INSTALL_RPATH ${_rpath})
install(
TARGETS
AppImageLauncher
RUNTIME DESTINATION ${_bindir} COMPONENT APPIMAGELAUNCHER
LIBRARY DESTINATION ${_libdir} COMPONENT APPIMAGELAUNCHER
)
endif()
# AppImageLauncherSettings application
add_executable(AppImageLauncherSettings settings_main.cpp resources.qrc settings_dialog.ui settings_dialog.cpp)
target_link_libraries(AppImageLauncherSettings shared)
# set binary runtime rpath to make sure the libappimage.so built and installed by this project is going to be used
# by the installed binaries (be it the .deb, the AppImage, or whatever)
# in order to make the whole install tree relocatable, a relative path is used
set_target_properties(AppImageLauncherSettings PROPERTIES INSTALL_RPATH ${_rpath})
install(
TARGETS
AppImageLauncherSettings
RUNTIME DESTINATION ${_bindir} COMPONENT APPIMAGELAUNCHER
LIBRARY DESTINATION ${_libdir} COMPONENT APPIMAGELAUNCHER
)
# AppImage removal helper
add_executable(remove remove_main.cpp remove.ui resources.qrc)
target_link_libraries(remove shared translationmanager libappimage)
# see AppImageLauncher for a description
set_target_properties(remove PROPERTIES INSTALL_RPATH "\$ORIGIN")
install(
TARGETS
remove
RUNTIME DESTINATION ${_private_libdir} COMPONENT APPIMAGELAUNCHER
)
# AppImage update helper
if(ENABLE_UPDATE_HELPER)
add_executable(update update.ui update_main.cpp resources.qrc)
target_link_libraries(update shared translationmanager libappimage libappimageupdate-qt Qt5::Quick Qt5::QuickWidgets Qt5::Qml)
# see AppImageLauncher for a description
set_target_properties(update PROPERTIES INSTALL_RPATH "\$ORIGIN")
install(
TARGETS
update
RUNTIME DESTINATION ${_private_libdir} COMPONENT APPIMAGELAUNCHER
)
endif()
07070100000038000081a400000000000000000000000168cf694000001467000000000000000000000000000000000000001500000000src/ui/first-run.cpp// system includes
#include <stdexcept>
// library includes
#include <QDesktopServices>
#include <QDialog>
#include <QDialogButtonBox>
#include <QLabel>
#include <QDebug>
#include <QFileDialog>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsPixmapItem>
#include <QImage>
#include <QLayout>
#include <QStyle>
#include <QUrl>
// local includes
#include "ui_first-run.h"
#include "shared.h"
class FirstRunDialog : public QDialog {
private:
Ui::FirstRunDialog* firstRunDialog{};
// stores custom destination dir for AppImages
// if this is empty, the default value (defined in-code, might change over time) will be chosen
// the default will not be written to the config file, hence we need a way to detect that state, and it's assumed
// that when this string is empty, that's the case
// we could also just compare this directory with the default value just before saving, but IMO it's more obvious
// to the user that the "default" state is lost after having saved something with the "choose" button in a file
// dialog
QString destinationDir;
private Q_SLOTS:
void resetDefaults() {
firstRunDialog->askMoveCheckBox->setChecked(true);
destinationDir = "";
updateDestinationDirLabel();
}
void handleButtonClicked(QAbstractButton* button) {
if (button == firstRunDialog->buttonBox->button(QDialogButtonBox::RestoreDefaults)) {
qDebug() << "restore defaults";
resetDefaults();
} else if (button == firstRunDialog->buttonBox->button(QDialogButtonBox::Help)) {
qDebug() << "help";
QDesktopServices::openUrl(QUrl("https://github.com/TheAssassin/AppImageLauncher/wiki/First-run"));
} else {
qDebug() << "unknown button clicked" << button;
}
}
void handleAskMoveCheckBoxStateChange(int state) {
qDebug() << "new ask move check box state" << state;
// this alone unfortunately doesn't do the trick...
for (auto* layout : {
static_cast<QLayout*>(firstRunDialog->destDirVertLayout),
static_cast<QLayout*>(firstRunDialog->destDirHorLayout),
}) {
layout->setEnabled(state > 0);
}
// have to also manually enable/disable all the
for (auto* label : {
static_cast<QWidget*>(firstRunDialog->destinationDirDescLabel),
static_cast<QWidget*>(firstRunDialog->destinationDirLabel),
static_cast<QWidget*>(firstRunDialog->customizeIntegrationDirButton),
}) {
label->setEnabled(state > 0);
}
}
void handleCustomizeIntegrationDirButtonClicked(bool checked = false) {
(void) checked;
auto oldDir = destinationDir;
if (oldDir.isEmpty())
oldDir = integratedAppImagesDestination().absolutePath();
auto newDir = QFileDialog::getExistingDirectory(this, tr("Choose integration destination dir"), oldDir);
// the call above returns an empty string if the user aborts the dialog
if (!newDir.isEmpty()) {
destinationDir = newDir;
}
// updating never is a bad idea
updateDestinationDirLabel();
}
private:
void updateDestinationDirLabel() {
QString text = destinationDir;
// fallback to default
if (text.isEmpty())
text = integratedAppImagesDestination().absolutePath() + " " + tr("(default)");
firstRunDialog->destinationDirLabel->setText(text);
}
void initUi() {
firstRunDialog = new Ui::FirstRunDialog;
// setupUi will modify this dialog so that it looks just like what we designed in Qt Designer
firstRunDialog->setupUi(this);
// set up logo in a QLabel
firstRunDialog->logoLabel->setText("");
auto pixmap = QPixmap::fromImage(QImage(":/AppImageLauncher.svg")).scaled(QSize(128,128),
Qt::KeepAspectRatio, Qt::SmoothTransformation
);
firstRunDialog->logoLabel->setPixmap(pixmap);
// setting icon in Qt Designer doesn't seem to work
firstRunDialog->customizeIntegrationDirButton->setIcon(this->style()->standardIcon(QStyle::SP_DirIcon));
// reset defaults
resetDefaults();
// set up all connections
connect(firstRunDialog->buttonBox, &QDialogButtonBox::clicked, this, &FirstRunDialog::handleButtonClicked);
connect(firstRunDialog->askMoveCheckBox, &QCheckBox::stateChanged, this, &FirstRunDialog::handleAskMoveCheckBoxStateChange);
connect(firstRunDialog->customizeIntegrationDirButton, &QPushButton::clicked, this, &FirstRunDialog::handleCustomizeIntegrationDirButtonClicked);
}
public:
FirstRunDialog() {
initUi();
}
void writeConfigFile() {
bool askToMove = firstRunDialog->askMoveCheckBox->checkState() == Qt::Checked;
createConfigFile(askToMove ? 1 : 0, destinationDir, -1);
}
};
void showFirstRunDialog() {
auto dialog = new FirstRunDialog;
setUpFallbackIconPaths(dialog);
auto rv = dialog->exec();
if (rv <= 0) {
QApplication::exit(3);
exit(3);
}
dialog->writeConfigFile();
}
07070100000039000081a400000000000000000000000168cf69400000001b000000000000000000000000000000000000001300000000src/ui/first-run.hvoid showFirstRunDialog();
0707010000003a000081a400000000000000000000000168cf6940000018c4000000000000000000000000000000000000001400000000src/ui/first-run.ui<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>FirstRunDialog</class>
<widget class="QDialog" name="FirstRunDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>532</width>
<height>300</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>First run</string>
</property>
<property name="sizeGripEnabled">
<bool>true</bool>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="logoLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>130</width>
<height>130</height>
</size>
</property>
<property name="text">
<string notr="true"/>
</property>
<property name="pixmap">
<pixmap>:/AppImageLauncher.svg</pixmap>
</property>
<property name="scaledContents">
<bool>false</bool>
</property>
<property name="alignment">
<set>Qt::AlignHCenter|Qt::AlignTop</set>
</property>
<property name="margin">
<number>8</number>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="welcomeTextLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="text">
<string><html><head/><body><p><span style=" font-size:11pt; font-weight:600;">Welcome to AppImageLauncher!</span></p><p>This little helper is designed to improve your AppImage experience on your computer.</p><p>It appears you have never run AppImageLauncher before. Please take a minute and configure your preferences. You can always change these later on, using the control panel.</p></body></html></string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="margin">
<number>2</number>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QCheckBox" name="askMoveCheckBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Ask me whether to move new AppImages into a central location</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<layout class="QVBoxLayout" name="destDirVertLayout">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="destinationDirDescLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Integration target destination directory:</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="destDirHorLayout">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<item>
<widget class="QLabel" name="destinationDirLabel">
<property name="font">
<font>
<family>DejaVu Sans Mono</family>
</font>
</property>
<property name="text">
<string notr="true">path placeholder</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="customizeIntegrationDirButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>15</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Customize</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item row="3" column="0" colspan="2">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::RestoreDefaults</set>
</property>
<property name="centerButtons">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>FirstRunDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>FirstRunDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
0707010000003b000081a400000000000000000000000168cf694000000755000000000000000000000000000000000000001e00000000src/ui/integration_dialog.cpp// system includes
#include <sstream>
#include <utility>
// library includes
#include <QStyle>
// local headers
#include "integration_dialog.h"
#include "ui_integration_dialog.h"
IntegrationDialog::IntegrationDialog(QString pathToAppImage, QString integratedAppImagesDestinationPath,
QWidget* parent) :
QDialog(parent), ui(new Ui::IntegrationDialog),
pathToAppImage(std::move(pathToAppImage)),
integratedAppImagesDestinationPath(std::move(integratedAppImagesDestinationPath)) {
ui->setupUi(this);
setIcon();
setMessage();
QObject::connect(ui->pushButtonIntegrateAndRun, &QPushButton::released, this,
&IntegrationDialog::onPushButtonIntegrateAndRunReleased);
QObject::connect(ui->pushButtonRunOnce, &QPushButton::released, this,
&IntegrationDialog::onPushButtonRunOnceReleased);
// make translation fit by adjusting the minimum size of the message label to the size calculated by Qt
ui->message->setMinimumSize(ui->message->sizeHint());
}
void IntegrationDialog::setMessage() {
QString message = ui->message->text();
message = message.arg(pathToAppImage, integratedAppImagesDestinationPath);
ui->message->setText(message);
}
void IntegrationDialog::setIcon() {
QIcon icon = QIcon(":/AppImageLauncher.svg");
QPixmap pixmap = icon.pixmap(QSize(64, 64));
ui->icon->setPixmap(pixmap);
}
IntegrationDialog::~IntegrationDialog() {
delete ui;
}
void IntegrationDialog::onPushButtonIntegrateAndRunReleased() {
this->resultAction = ResultingAction::IntegrateAndRun;
this->accept();
}
void IntegrationDialog::onPushButtonRunOnceReleased() {
this->resultAction = ResultingAction::RunOnce;
this->accept();
}
IntegrationDialog::ResultingAction IntegrationDialog::getResultAction() const {
return resultAction;
}
0707010000003c000081a400000000000000000000000168cf6940000003b8000000000000000000000000000000000000001c00000000src/ui/integration_dialog.h#ifndef APPIMAGELAUNCHER_INTEGRATION_DIALOG_H
#define APPIMAGELAUNCHER_INTEGRATION_DIALOG_H
// library includes
#include <QDialog>
QT_BEGIN_NAMESPACE
namespace Ui { class IntegrationDialog; }
QT_END_NAMESPACE
class IntegrationDialog : public QDialog {
Q_OBJECT
public:
enum ResultingAction {
IntegrateAndRun,
RunOnce
};
explicit IntegrationDialog(QString pathToAppImage, QString integratedAppImagesDestinationPath,
QWidget* parent = nullptr);
~IntegrationDialog() override;
ResultingAction getResultAction() const;
protected:
Q_SLOT void onPushButtonIntegrateAndRunReleased();
Q_SLOT void onPushButtonRunOnceReleased();
ResultingAction resultAction;
private:
Ui::IntegrationDialog* ui;
QString pathToAppImage;
QString integratedAppImagesDestinationPath;
void setIcon();
void setMessage();
};
#endif //APPIMAGELAUNCHER_INTEGRATION_DIALOG_H
0707010000003d000081a400000000000000000000000168cf694000001379000000000000000000000000000000000000001d00000000src/ui/integration_dialog.ui<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>IntegrationDialog</class>
<widget class="QDialog" name="IntegrationDialog">
<property name="windowModality">
<enum>Qt::WindowModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>460</width>
<height>300</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Desktop Integration</string>
</property>
<property name="sizeGripEnabled">
<bool>false</bool>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="icon">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Icon</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="margin">
<number>12</number>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="message">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
<html><head><meta name="qrichtext" content="1" /><style type="text/css">
p, li { white-space: pre-wrap; }
</style></head><body style=" font-family:'Noto Sans'; font-size:10pt; font-weight:400; font-style:normal;">
<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">%1 has not been integrated into your system.</p>
<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"> <br />Integrating it will move the AppImage into a predefined location, and include it in your application launcher.</p>
<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p>
<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">To remove or update the AppImage, please use the context menu of the application icon in your task bar or launcher. </p>
<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p>
<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">The directory where the integrated AppImages are stored in is currently set to: %2</p></body></html></string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="margin">
<number>0</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="pushButtonIntegrateAndRun">
<property name="text">
<string>Integrate and run</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButtonRunOnce">
<property name="text">
<string>Run once</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
0707010000003e000081a400000000000000000000000168cf69400000447b000000000000000000000000000000000000001000000000src/ui/main.cpp// system includes
#include <fstream>
#include <iostream>
#include <sstream>
extern "C" {
#include <sys/stat.h>
#include <libgen.h>
#include <unistd.h>
#include <glib.h>
}
// library includes
#include <QApplication>
#include <QCommandLineParser>
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QFileDialog>
#include <QFileInfo>
#include <QMessageBox>
#include <QProcess>
#include <QPushButton>
#include <QRegularExpression>
#include <QString>
extern "C" {
#include <appimage/appimage.h>
}
// local headers
#include "shared.h"
#include "trashbin.h"
#include "translationmanager.h"
#include "first-run.h"
#include "integration_dialog.h"
// Runs an AppImage. Returns suitable exit code for main application.
int runAppImage(const QString& pathToAppImage, unsigned long argc, char** argv) {
// needs to be converted to std::string to be able to use c_str()
// when using QString and then .toStdString().c_str(), the std::string instance will be an rvalue, and the
// pointer returned by c_str() will be invalid
auto fullPathToAppImage = QFileInfo(pathToAppImage).absoluteFilePath();
auto type = appimage_get_type(fullPathToAppImage.toStdString().c_str(), false);
if (type < 1 || type > 3) {
displayError(QObject::tr("AppImageLauncher does not support type %1 AppImages at the moment.").arg(type));
return 1;
}
// first of all, chmod +x the AppImage registerFile
// be happy the registerFile is executable already
if (!makeExecutable(fullPathToAppImage)) {
displayError(QObject::tr("Could not make AppImage executable: %1").arg(fullPathToAppImage));
return 1;
}
// suppress desktop integration script etc.
setenv("DESKTOPINTEGRATION", "AppImageLauncher", true);
auto makeVectorBuffer = [](const std::string& str) {
std::vector<char> strBuffer(str.size() + 1, '\0');
strncpy(strBuffer.data(), str.c_str(), str.size());
return strBuffer;
};
// calculate buffer to bypass binary
std::string pathToBinfmtBypassLauncher = privateLibDirPath("binfmt-bypass").toStdString() + "/binfmt-bypass";
// create new args array for exec()d process
std::vector<char*> args;
// first argument is the path to our launcher
auto pathToBinfmtBypassLauncherBuffer = makeVectorBuffer(pathToBinfmtBypassLauncher);
args.push_back(pathToBinfmtBypassLauncherBuffer.data());
// first argument is consumed by the bypass launcher
// the reason we launch the bypass launcher as a new process to save RAM (we have to launch the actual AppImage
// as a subprocess, and the launcher executable has a much lower memory footprint)
auto pathToAppImageBuffer = makeVectorBuffer(pathToAppImage.toStdString());
args.push_back(pathToAppImageBuffer.data());
// copy arguments
for (unsigned long i = 1; i < argc; i++) {
args.push_back(argv[i]);
}
// args need to be null terminated
args.push_back(nullptr);
execv(pathToBinfmtBypassLauncher.c_str(), args.data());
const auto& error = errno;
std::cerr << QObject::tr("execv() failed: %1").arg(strerror(error)).toStdString() << std::endl;
return 1;
}
// factory method to build and return a suitable Qt application instance
// it remembers a previously created instance, and will return it if available
// otherwise a new one is created and configure
// caution: cannot use <widget>.exec() any more, instead call <widget>.show() and use QApplication::exec()
QCoreApplication* getApp(char** argv) {
if (QCoreApplication::instance() != nullptr)
return QCoreApplication::instance();
// build application version string
std::string version;
{
std::ostringstream oss;
oss << "version " << APPIMAGELAUNCHER_VERSION << " "
<< "(git commit " << APPIMAGELAUNCHER_GIT_COMMIT << "), built on "
<< APPIMAGELAUNCHER_BUILD_DATE;
version = oss.str();
}
QCoreApplication* app;
// need to pass rvalue, hence defining a variable
int* fakeArgc = new int{1};
static char** fakeArgv = new char*{strdup(argv[0])};
if (isHeadless()) {
app = new QCoreApplication(*fakeArgc, fakeArgv);
} else {
auto uiApp = new QApplication(*fakeArgc, fakeArgv);
QApplication::setApplicationDisplayName("AppImageLauncher");
// this doesn't seem to have any effect... but it doesn't hurt either
uiApp->setWindowIcon(QIcon(":/AppImageLauncher.svg"));
app = uiApp;
}
QCoreApplication::setApplicationName("AppImageLauncher");
QCoreApplication::setApplicationVersion(QString::fromStdString(version));
return app;
}
int main(int argc, char** argv) {
// create a suitable application object (either graphical (QApplication) or headless (QCoreApplication))
// Use a fake argc value to avoid QApplication from modifying the arguments
QCoreApplication* app = getApp(argv);
// install translations
TranslationManager translationManager(*app);
// clean up old desktop files
if (!cleanUpOldDesktopIntegrationResources()) {
displayError(QObject::tr("Failed to clean up old desktop files"));
return 1;
}
// clean up trash directory
{
TrashBin bin;
if (!bin.cleanUp()) {
displayError(QObject::tr("Failed to clean up AppImage trash bin: %1").arg(bin.path()));
}
}
std::ostringstream usage;
usage << QObject::tr("Usage: %1 [options] <path>").arg(argv[0]).toStdString() << std::endl
<< QObject::tr("Desktop integration helper for AppImages, for use by Linux distributions.").toStdString()
<< std::endl
<< std::endl
<< QObject::tr("Options:").toStdString() << std::endl
<< " --appimagelauncher-help " << QObject::tr("Display this help and exit").toStdString() << std::endl
<< " --appimagelauncher-version " << QObject::tr("Display version and exit").toStdString() << std::endl
<< std::endl
<< QObject::tr("Arguments:").toStdString() << std::endl
<< " path " << QObject::tr("Path to AppImage (mandatory)").toStdString() << std::endl;
auto displayVersion = [&app]() {
std::cerr << "AppImageLauncher " << app->applicationVersion().toStdString() << std::endl;
};
// display usage and exit if path to AppImage is missing
if (argc <= 1) {
displayVersion();
std::cerr << std::endl;
std::cerr << usage.str();
return 1;
}
std::vector<char*> appImageArgv;
// search for --appimagelauncher-* arguments in args list
for (int i = 1; i < argc; i++) {
QString arg = argv[i];
// reserved argument space
const QString prefix = "--appimagelauncher-";
if (arg.startsWith(prefix)) {
if (arg == prefix + "help") {
displayVersion();
std::cerr << std::endl;
std::cerr << usage.str();
return 0;
} else if (arg == prefix + "version") {
displayVersion();
return 0;
} else if (arg == prefix + "cleanup") {
// exit immediately after cleanup
return 0;
} else {
std::cerr << QObject::tr("Unknown AppImageLauncher option: %1").arg(arg).toStdString() << std::endl;
return 1;
}
} else {
appImageArgv.emplace_back(argv[i]);
}
}
// sanitize path
auto pathToAppImage = QDir(QString(argv[1])).absolutePath();
if (!QFile(pathToAppImage).exists()) {
displayError(QObject::tr("Error: no such file or directory: %1").arg(pathToAppImage));
return 1;
}
// if the users wishes to disable AppImageLauncher, we just run the AppImage as-ish
// also we don't ever want to integrate symlinks (see #290 for more information)
if (getenv("APPIMAGELAUNCHER_DISABLE") != nullptr || QFileInfo(pathToAppImage).isSymLink()) {
return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data());
}
const auto type = appimage_get_type(pathToAppImage.toStdString().c_str(), false);
if (type <= 0 || type > 2) {
displayError(QObject::tr("Not an AppImage: %1").arg(pathToAppImage));
return 1;
}
// type 2 specific checks
if (type == 2) {
// check parameters
{
for (int i = 0; i < argc; i++) {
QString arg = argv[i];
// reserved argument space
const QString prefix = "--appimage-";
if (arg.startsWith(prefix)) {
// don't annoy users who try to mount or extract AppImages
if (arg == prefix + "mount" || arg == prefix + "extract" || arg == prefix + "updateinformation") {
return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data());
}
}
}
}
}
// enable and start/disable and stop appimagelauncherd service
auto config = getConfig();
// assumes defaults if config doesn't exist or lacks the related key(s)
if (config == nullptr || !config->contains("AppImageLauncher/enable_daemon") ||
config->value("AppImageLauncher/enable_daemon").toBool()) {
system("systemctl --user enable appimagelauncherd.service");
system("systemctl --user start appimagelauncherd.service");
} else {
system("systemctl --user disable appimagelauncherd.service");
system("systemctl --user stop appimagelauncherd.service");
}
// beyond the next block, the code requires a UI
// as we don't want to offer integration over a headless connection, we just run the AppImage
if (isHeadless()) {
return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data());
}
// if config doesn't exist, create a default one
if (config == nullptr) {
showFirstRunDialog();
config = getConfig();
}
if (config == nullptr) {
displayError("Could not read config file");
}
// if the user opted out of the "ask move" thing, we can just run the AppImage
if (config->contains("AppImageLauncher/ask_to_move") && !config->value("AppImageLauncher/ask_to_move").toBool()) {
return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data());
}
// check for X-AppImage-Integrate=false
auto shallNotBeIntegrated = appimage_shall_not_be_integrated(pathToAppImage.toStdString().c_str());
if (shallNotBeIntegrated < 0)
std::cerr << "AppImageLauncher error: appimage_shall_not_be_integrated() failed (returned "
<< shallNotBeIntegrated << ")" << std::endl;
else if (shallNotBeIntegrated > 0)
return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data());
// AppImages in AppImages are not supposed to be integrated
if (pathToAppImage.startsWith("/tmp/.mount_"))
return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data());
// ignore terminal apps (fixes #2)
auto isTerminalApp = appimage_is_terminal_app(pathToAppImage.toStdString().c_str());
if (isTerminalApp < 0)
std::cerr << "AppImageLauncher error: appimage_is_terminal_app() failed (returned " << isTerminalApp << ")"
<< std::endl;
else if (isTerminalApp > 0)
return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data());
// AppImages in AppImages are not supposed to be integrated
if (pathToAppImage.startsWith("/tmp/.mount_"))
return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data());
const auto pathToIntegratedAppImage = buildPathToIntegratedAppImage(pathToAppImage);
auto integrateAndRunAppImage = [&pathToAppImage, &pathToIntegratedAppImage, &appImageArgv]() {
// check whether integration was successful
auto rv = integrateAppImage(pathToAppImage, pathToIntegratedAppImage);
// make sure the icons in the launcher are refreshed
if (!updateDesktopDatabaseAndIconCaches())
return 1;
if (rv == INTEGRATION_FAILED) {
return 1;
} else if (rv == INTEGRATION_ABORTED) {
return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data());
} else {
return runAppImage(pathToIntegratedAppImage, appImageArgv.size(), appImageArgv.data());
}
};
// after checking whether the AppImage can/must be run without integrating it, we now check whether it actually
// has been integrated already
if (hasAlreadyBeenIntegrated(pathToAppImage)) {
auto updateAndRunAppImage = [&pathToAppImage, &appImageArgv]() {
// in case there was an update of AppImageLauncher, we should should also update the desktop database
// and icon caches
if (!desktopFileHasBeenUpdatedSinceLastUpdate(pathToAppImage)) {
if (!updateDesktopFileAndIcons(pathToAppImage))
return 1;
// make sure the icons in the launcher are refreshed after updating the desktop file
if (!updateDesktopDatabaseAndIconCaches())
return 1;
}
return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data());
};
// assume we have to ask
// prove me wrong!
bool needToAskAboutMoving = true;
// okay, I'll try to prove you wrong
{
auto directoriesNotToAskAboutMovingFor = daemonDirectoriesToWatch(config);
// normally the main integration destination should be contained
// but bugs happen, and we want to be sure not to create a weird situation where you'd be asked about
// moving files into yet the directory you want to move them into
directoriesNotToAskAboutMovingFor.insert(integratedAppImagesDestination());
for (const auto& dir : directoriesNotToAskAboutMovingFor) {
if (isInDirectory(pathToAppImage, dir)) {
needToAskAboutMoving = false;
break;
}
}
}
// not so fast: even if it's not in the main integration directory, there's more viable locations where
// AppImages may reside just fine
if (needToAskAboutMoving) {
for (const auto& additionalLocation : additionalAppImagesLocations()) {
if (isInDirectory(pathToAppImage, additionalLocation)) {
needToAskAboutMoving = false;
}
}
}
if (needToAskAboutMoving) {
auto* messageBox = new QMessageBox(
QMessageBox::Warning,
QMessageBox::tr("Warning"),
QMessageBox::tr("AppImage %1 has already been integrated, but it is not in the current integration "
"destination directory."
"\n\n"
"Do you want to move it into the new destination?"
"\n\n"
"Choosing No will run the AppImage once, and leave the AppImage in its current "
"directory."
"\n\n").arg(pathToAppImage) +
// translate separately to share string with the other dialog
QObject::tr("The directory the integrated AppImages are stored in is currently set to:\n"
"%1").arg(integratedAppImagesDestination().path()) + "\n",
QMessageBox::Yes | QMessageBox::No
);
messageBox->setDefaultButton(QMessageBox::Yes);
messageBox->show();
QApplication::exec();
// if the user selects No, then continue as if the AppImage would not be in this directory
if (messageBox->clickedButton() == messageBox->button(QMessageBox::Yes)) {
// unregister AppImage, move, and re-integrate
if (appimage_unregister_in_system(pathToAppImage.toStdString().c_str(), false) != 0) {
displayError(QMessageBox::tr("Failed to unregister AppImage before re-integrating it"));
return 1;
}
return integrateAndRunAppImage();
} else {
return updateAndRunAppImage();
}
} else {
return updateAndRunAppImage();
}
}
QString integratedAppImagesDestinationPath = integratedAppImagesDestination().path();
auto integrationDialog = new IntegrationDialog(pathToAppImage, integratedAppImagesDestinationPath);
integrationDialog->show();
// As the integration dialog is the only window in our application we can safely use its exec method
integrationDialog->exec();
if (integrationDialog->result() == QDialog::Rejected)
return 0;
switch (integrationDialog->getResultAction()) {
case IntegrationDialog::IntegrateAndRun:
return integrateAndRunAppImage();
case IntegrationDialog::RunOnce:
return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data());
default:
displayError(QObject::tr("Unexpected result from the integration dialog."));
return 1;
}
}
0707010000003f000081a400000000000000000000000168cf694000000b1c000000000000000000000000000000000000001100000000src/ui/remove.ui<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>RemoveDialog</class>
<widget class="QDialog" name="RemoveDialog">
<property name="windowModality">
<enum>Qt::WindowModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>415</width>
<height>96</height>
</rect>
</property>
<property name="windowTitle">
<string>Delete AppImage</string>
</property>
<property name="locale">
<locale language="English" country="UnitedStates"/>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout">
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<item>
<widget class="QLabel" name="messageLabel">
<property name="text">
<string><html><head/><body><p>Are you sure you want to delete this AppImage?</p></body></html></string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="pathLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>389</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>%1</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>RemoveDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>RemoveDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
07070100000040000081a400000000000000000000000168cf6940000010c6000000000000000000000000000000000000001700000000src/ui/remove_main.cpp// system includes
#include <iostream>
#include <sstream>
// library includes
#include <QApplication>
#include <QCommandLineParser>
#include <QDir>
#include <QFile>
#include <QFileDialog>
#include <QFileInfo>
#include <QLibraryInfo>
#include <QMessageBox>
#include <QObject>
#include <QPushButton>
#include <QTranslator>
extern "C" {
#include <appimage/appimage.h>
}
// local includes
#include "shared.h"
#include "translationmanager.h"
#include "trashbin.h"
#include "ui_remove.h"
int main(int argc, char** argv) {
QCommandLineParser parser;
parser.setApplicationDescription(QObject::tr("Helper to delete integrated AppImages easily, e.g., from the application launcher's context menu"));
QApplication app(argc, argv);
QApplication::setApplicationDisplayName("AppImageLauncher");
QApplication::setWindowIcon(QIcon(":/AppImageLauncher.svg"));
std::ostringstream version;
version << "version " << APPIMAGELAUNCHER_VERSION << " "
<< "(git commit " << APPIMAGELAUNCHER_GIT_COMMIT << "), built on "
<< APPIMAGELAUNCHER_BUILD_DATE;
QApplication::setApplicationVersion(QString::fromStdString(version.str()));
// install translations
TranslationManager translationManager(app);
parser.addHelpOption();
parser.addVersionOption();
parser.process(app);
parser.addPositionalArgument("path", QObject::tr("Path to AppImage"), QObject::tr("<path>"));
if (parser.positionalArguments().empty()) {
parser.showHelp(1);
}
const auto pathToAppImage = parser.positionalArguments().first();
if (!QFile(pathToAppImage).exists()) {
QMessageBox::critical(nullptr, "Error", QObject::tr("Error: no such file or directory: %1").arg(pathToAppImage));
return 1;
}
checkAuthorizationAndShowDialogIfNecessary(pathToAppImage, "Delete anyway?");
const auto type = appimage_get_type(pathToAppImage.toStdString().c_str(), false);
if (type <= 0 || type > 2) {
QMessageBox::critical(
nullptr,
QObject::tr("AppImage delete helper error"),
QObject::tr("Not an AppImage:\n\n%1").arg(pathToAppImage)
);
return 1;
}
// this tool should not do anything if the file isn't integrated
// the file is only supposed to work on integrated AppImages and _nothing else_
// if (!hasAlreadyBeenIntegrated(pathToAppImage)) {
// QMessageBox::critical(
// nullptr,
// QObject::tr("AppImage delete helper error"),
// QObject::tr("Refusing to work on non-integrated AppImage:\n\n%1").arg(pathToAppImage)
// );
// return 1;
// }
QDialog dialog;
Ui::RemoveDialog ui;
ui.setupUi(&dialog);
ui.pathLabel->setText(pathToAppImage);
// must be done *after* loading the UI into the dialog
setUpFallbackIconPaths(&dialog);
auto rv = dialog.exec();
// check if user has canceled the dialog
// a confirmation would result in an exit code of 1
if (rv != 1) {
return 0;
}
// first, unregister AppImage
if (!unregisterAppImage(pathToAppImage)) {
QMessageBox::critical(
nullptr,
QObject::tr("Error"),
QObject::tr("Failed to unregister AppImage: %1").arg(pathToAppImage)
);
return 1;
}
TrashBin bin;
// now, move AppImage into trash bin
if (!bin.disposeAppImage(pathToAppImage)) {
QMessageBox::critical(
nullptr,
QObject::tr("Error"),
QObject::tr("Failed to move AppImage into trash bin directory")
);
return 1;
}
// run clean up cycle for trash bin
// if the current AppImage is ready to be deleted, this call will immediately remove it from the system
// otherwise, it'll be cleaned up at some subsequent run of AppImageLauncher or the removal tool
if (!bin.cleanUp()) {
QMessageBox::critical(
nullptr,
QObject::tr("Error"),
QObject::tr("Failed to clean up AppImage trash bin: %1").arg(bin.path())
);
return 1;
}
// update desktop database and icon caches
if (!updateDesktopDatabaseAndIconCaches())
return 1;
return 0;
}
07070100000041000081a400000000000000000000000168cf694000000101000000000000000000000000000000000000001500000000src/ui/resources.qrc<!DOCTYPE RCC>
<RCC version="1.0">
<qresource>
<file alias="AppImageLauncher.svg">../../resources/icons/hicolor/scalable/apps/AppImageLauncher.svg</file>
<file alias="update_spinner.qml">update_spinner.qml</file>
</qresource>
</RCC>
07070100000042000081a400000000000000000000000168cf69400000210d000000000000000000000000000000000000001b00000000src/ui/settings_dialog.cpp// libraries
#include <QDebug>
#include <QFileDialog>
#include <QFileIconProvider>
#include <QStandardPaths>
// local
#include "settings_dialog.h"
#include "ui_settings_dialog.h"
#include "shared.h"
SettingsDialog::SettingsDialog(QWidget* parent) : QDialog(parent), ui(new Ui::SettingsDialog),
settingsFile(getConfig(this)) {
ui->setupUi(this);
ui->applicationsDirLineEdit->setPlaceholderText(integratedAppImagesDestination().absolutePath());
loadSettings();
// cosmetic changes in lite mode
#ifdef BUILD_LITE
ui->daemonIsEnabledCheckBox->setChecked(true);
ui->daemonIsEnabledCheckBox->setEnabled(false);
ui->askMoveCheckBox->setChecked(false);
ui->askMoveCheckBox->setEnabled(false);
#endif
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &SettingsDialog::onDialogAccepted);
connect(ui->chooseAppsDirToolButton, &QToolButton::released, this, &SettingsDialog::onChooseAppsDirClicked);
connect(ui->additionalDirsAddButton, &QToolButton::released, this, &SettingsDialog::onAddDirectoryToWatchButtonClicked);
connect(ui->additionalDirsRemoveButton, &QToolButton::released, this, &SettingsDialog::onRemoveDirectoryToWatchButtonClicked);
connect(ui->additionalDirsListWidget, &QListWidget::itemActivated, this, &SettingsDialog::onDirectoryToWatchItemActivated);
connect(ui->additionalDirsListWidget, &QListWidget::itemClicked, this, &SettingsDialog::onDirectoryToWatchItemActivated);
QStringList availableFeatures;
#ifdef ENABLE_UPDATE_HELPER
availableFeatures << "<span style='color: green;'>✔</span> " + tr("updater available for AppImages supporting AppImageUpdate");
#else
availableFeatures << "<span style='color: red;'>🞬</span> " + tr("updater unavailable");
#endif
#ifdef BUILD_LITE
availableFeatures << "<br /><br />"
<< tr("<strong>Note: this is an AppImageLauncher Lite build, only supports a limited set of features</strong><br />"
"Please install the full version via the provided native packages to enjoy the full AppImageLauncher experience");
#endif
ui->featuresLabel->setText(availableFeatures.join('\n'));
// no matter what tab was selected when saving in Qt designer, we want to start up with the first tab
ui->tabWidget->setCurrentWidget(ui->launcherTab);
}
SettingsDialog::~SettingsDialog() {
delete ui;
}
void SettingsDialog::addDirectoryToWatchToListView(const QString& dirPath) {
// empty paths are not permitted
if (dirPath.isEmpty())
return;
const QDir dir(dirPath);
// we don't want to redundantly add the main integration directory
if (dir == integratedAppImagesDestination())
return;
QIcon icon;
auto findIcon = [](const std::initializer_list<QString>& names) {
for (const auto& i : names) {
auto icon = QIcon::fromTheme(i, loadIconWithFallback(i));
if (!icon.isNull())
return icon;
}
return QIcon{};
};
if (dir.exists()) {
icon = findIcon({"folder"});
} else {
// TODO: search for more meaningful icon, "remove" doesn't really show the directory is missing
icon = findIcon({"remove"});
}
if (icon.isNull()) {
qDebug() << "item icon unavailable, using fallback";
}
auto* item = new QListWidgetItem(icon, dirPath);
ui->additionalDirsListWidget->addItem(item);
}
void SettingsDialog::loadSettings() {
const auto daemonIsEnabled = settingsFile->value("AppImageLauncher/enable_daemon", "true").toBool();
const auto askMoveChecked = settingsFile->value("AppImageLauncher/ask_to_move", "true").toBool();
if (settingsFile) {
ui->daemonIsEnabledCheckBox->setChecked(daemonIsEnabled);
ui->askMoveCheckBox->setChecked(askMoveChecked);
ui->applicationsDirLineEdit->setText(settingsFile->value("AppImageLauncher/destination").toString());
const auto additionalDirsPath = settingsFile->value("appimagelauncherd/additional_directories_to_watch", "").toString();
for (const auto& dirPath : additionalDirsPath.split(":")) {
addDirectoryToWatchToListView(dirPath);
}
}
}
void SettingsDialog::onDialogAccepted() {
saveSettings();
toggleDaemon();
}
void SettingsDialog::saveSettings() {
QStringList additionalDirsToWatch;
{
QListWidgetItem* currentItem;
for (int i = 0; (currentItem = ui->additionalDirsListWidget->item(i)) != nullptr; ++i) {
additionalDirsToWatch << currentItem->text();
}
}
// temporary workaround to fill in the monitorMountedFilesystems with the same value it had in the old settings
// this is supposed to support the option while hiding it in the settings
int monitorMountedFilesystems = -1;
{
const auto oldSettings = getConfig();
static constexpr auto oldKey = "appimagelauncherd/monitor_mounted_filesystems";
// getConfig might return a null pointer if the config file doesn't exist
// we have to handle this, obviously
if (oldSettings != nullptr && oldSettings->contains(oldKey)) {
const auto oldValue = oldSettings->value(oldKey).toBool();
monitorMountedFilesystems = oldValue ? 1 : 0;
}
}
createConfigFile(ui->askMoveCheckBox->isChecked(),
ui->applicationsDirLineEdit->text(),
ui->daemonIsEnabledCheckBox->isChecked(),
additionalDirsToWatch,
monitorMountedFilesystems);
// reload settings
loadSettings();
}
void SettingsDialog::toggleDaemon() {
// assumes defaults if config doesn't exist or lacks the related key(s)
if (settingsFile) {
if (settingsFile->value("AppImageLauncher/enable_daemon", "true").toBool()) {
system("systemctl --user enable appimagelauncherd.service");
// we want to actually restart the service to apply the new configuration
system("systemctl --user restart appimagelauncherd.service");
} else {
system("systemctl --user disable appimagelauncherd.service");
system("systemctl --user stop appimagelauncherd.service");
}
}
}
void SettingsDialog::onChooseAppsDirClicked() {
QFileDialog fileDialog(this);
fileDialog.setFileMode(QFileDialog::DirectoryOnly);
fileDialog.setWindowTitle(tr("Select Applications directory"));
fileDialog.setDirectory(integratedAppImagesDestination().absolutePath());
// Gtk+ >= 3 segfaults when trying to use the native dialog, therefore we need to enforce the Qt one
// See #218 for more information
fileDialog.setOption(QFileDialog::DontUseNativeDialog, true);
if (fileDialog.exec()) {
QString dirPath = fileDialog.selectedFiles().first();
ui->applicationsDirLineEdit->setText(dirPath);
}
}
void SettingsDialog::onAddDirectoryToWatchButtonClicked() {
QFileDialog fileDialog(this);
fileDialog.setFileMode(QFileDialog::DirectoryOnly);
fileDialog.setWindowTitle(tr("Select additional directory to watch"));
fileDialog.setDirectory(QStandardPaths::locate(QStandardPaths::HomeLocation, ".", QStandardPaths::LocateDirectory));
// Gtk+ >= 3 segfaults when trying to use the native dialog, therefore we need to enforce the Qt one
// See #218 for more information
fileDialog.setOption(QFileDialog::DontUseNativeDialog, true);
if (fileDialog.exec()) {
QString dirPath = fileDialog.selectedFiles().first();
addDirectoryToWatchToListView(dirPath);
}
}
void SettingsDialog::onRemoveDirectoryToWatchButtonClicked() {
auto* widget = ui->additionalDirsListWidget;
auto* currentItem = widget->currentItem();
if (currentItem == nullptr)
return;
const auto index = widget->row(currentItem);
// after taking it, we have to delete it ourselves, Qt docs say
auto deletedItem = widget->takeItem(index);
delete deletedItem;
// we should deactivate the remove button once the last item is gone
if (widget->item(0) == nullptr) {
ui->additionalDirsRemoveButton->setEnabled(false);
}
}
void SettingsDialog::onDirectoryToWatchItemActivated(QListWidgetItem* item) {
// we activate the button based on whether there's an item selected
ui->additionalDirsRemoveButton->setEnabled(item != nullptr);
}
07070100000043000081a400000000000000000000000168cf694000000306000000000000000000000000000000000000001900000000src/ui/settings_dialog.h#pragma once
// system
#include <memory>
// libraries
#include <QDialog>
#include <QListWidgetItem>
#include <QSettings>
namespace Ui {
class SettingsDialog;
}
class SettingsDialog : public QDialog {
Q_OBJECT
public:
explicit SettingsDialog(QWidget* parent = nullptr);
~SettingsDialog() override;
protected slots:
void onChooseAppsDirClicked();
void onAddDirectoryToWatchButtonClicked();
void onRemoveDirectoryToWatchButtonClicked();
void onDirectoryToWatchItemActivated(QListWidgetItem* item);
void onDialogAccepted();
private:
void loadSettings();
void saveSettings();
void toggleDaemon();
void addDirectoryToWatchToListView(const QString& dirPath);
Ui::SettingsDialog* ui;
QSettings* settingsFile;
};
07070100000044000081a400000000000000000000000168cf694000002573000000000000000000000000000000000000001a00000000src/ui/settings_dialog.ui<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SettingsDialog</class>
<widget class="QDialog" name="SettingsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>539</width>
<height>459</height>
</rect>
</property>
<property name="windowTitle">
<string>AppImageLauncher Settings</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>1</number>
</property>
<widget class="QWidget" name="launcherTab">
<attribute name="title">
<string notr="true">AppImageLauncher</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QGroupBox" name="launcherGroupBox">
<property name="title">
<string>Launcher Dialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QCheckBox" name="askMoveCheckBox">
<property name="text">
<string>Ask whether to move AppImage files into the applications directory</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="integrationDirectoryGroupBox">
<property name="title">
<string>Location where to store your AppImage files to ease their management</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<layout class="QHBoxLayout" name="integrationGroupBoxLayout">
<item>
<widget class="QLineEdit" name="applicationsDirLineEdit">
<property name="text">
<string notr="true"/>
</property>
<property name="placeholderText">
<string>Applications directory path</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="chooseAppsDirToolButton">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="folder">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="availableFeaturesGroupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>80</height>
</size>
</property>
<property name="locale">
<locale language="English" country="UnitedStates"/>
</property>
<property name="title">
<string>Available Features</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="featuresLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<property name="text">
<string notr="true">TextLabel</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="daemonTab">
<attribute name="title">
<string notr="true">appimagelauncherd</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QGroupBox" name="daemonGeneralSettingsGroupBox">
<property name="locale">
<locale language="English" country="UnitedStates"/>
</property>
<property name="title">
<string>General settings</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<widget class="QCheckBox" name="daemonIsEnabledCheckBox">
<property name="toolTip">
<string><html><head/><body><p>When this box is checked, AppImageLauncher automatically starts a daemon called appimagelauncherd.</p><p>This daemon automatically integrates AppImages you copy into the &quot;Applications directory&quot; and the additional directories you configured. When the files are deleted, the daemon will clean up the integration data.</p></body></html></string>
</property>
<property name="text">
<string>Auto start auto-integration daemon</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="additionalDirsGroupBox">
<property name="locale">
<locale language="English" country="UnitedStates"/>
</property>
<property name="title">
<string>Additional directories to watch</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QListWidget" name="additionalDirsListWidget"/>
</item>
<item>
<layout class="QVBoxLayout" name="additionalDirsButtonsLayout">
<item>
<widget class="QToolButton" name="additionalDirsAddButton">
<property name="toolTip">
<string>Add new directory to list</string>
</property>
<property name="icon">
<iconset theme="list-add">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="additionalDirsRemoveButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Remove selected directory from list</string>
</property>
<property name="icon">
<iconset theme="list-remove">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Minimum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>SettingsDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>SettingsDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
07070100000045000081a400000000000000000000000168cf69400000026e000000000000000000000000000000000000001900000000src/ui/settings_main.cpp// libraries
#include <QApplication>
// local
#include <translationmanager.h>
#include <shared.h>
#include "settings_dialog.h"
int main(int argc, char** argv) {
QApplication app(argc, argv);
QApplication::setApplicationDisplayName("AppImageLauncher Settings");
QApplication::setWindowIcon(QIcon(":/AppImageLauncher.svg"));
TranslationManager mgr(app);
//
// // we ship some very basic fallbacks for icons used in the settings dialog
// // this should fix missing icons on some distros
SettingsDialog dialog;
setUpFallbackIconPaths(&dialog);
dialog.show();
return app.exec();
}
07070100000046000081a400000000000000000000000168cf694000000c7f000000000000000000000000000000000000001100000000src/ui/update.ui<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>UpdateDialog</class>
<widget class="QDialog" name="UpdateDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>168</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QStackedWidget" name="stackedWidget">
<property name="currentIndex">
<number>2</number>
</property>
<widget class="QWidget" name="spinnerPage">
<layout class="QVBoxLayout" name="verticalLayout_2" stretch="1,0">
<item>
<widget class="QQuickWidget" name="spinnerQuickWidget">
<property name="resizeMode">
<enum>QQuickWidget::SizeRootObjectToView</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Checking for update...</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="errorPage">
<layout class="QVBoxLayout" name="verticalLayout_4" stretch="0,1,0">
<item>
<widget class="QLabel" name="errorTitleLabel">
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string notr="true">title placeholder</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="errorMessageLabel">
<property name="text">
<string notr="true">message placeholder</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="errorButtonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="updaterPage">
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>QQuickWidget</class>
<extends>QWidget</extends>
<header location="global">QtQuickWidgets/QQuickWidget</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>
07070100000047000081a400000000000000000000000168cf694000001a31000000000000000000000000000000000000001700000000src/ui/update_main.cpp// system includes
#include <iostream>
#include <sstream>
// library includes
#include <QApplication>
#include <QCheckBox>
#include <QCommandLineParser>
#include <QFile>
#include <QLibraryInfo>
#include <QMessageBox>
#include <QPushButton>
#include <QTranslator>
#include <QThread>
extern "C" {
#include <appimage/appimage.h>
}
#include <appimage/update/qt-ui.h>
// local includes
#include "shared.h"
#include "translationmanager.h"
#include "ui_update.h"
using namespace appimage::update::qt;
class UpdateDialog : public QDialog {
Q_OBJECT
public:
explicit UpdateDialog(const QString& pathToAppImage) : _pathToAppImage(pathToAppImage), _ui(new Ui::UpdateDialog), _updater(new QtUpdater(pathToAppImage)) {
// configure UI
_ui->setupUi(this);
_ui->stackedWidget->setCurrentIndex(0);
// these three calls are needed to give the QQuickWidget the same background color as its parent window
_ui->spinnerQuickWidget->setAttribute(Qt::WA_AlwaysStackOnTop);
_ui->spinnerQuickWidget->setAttribute(Qt::WA_TranslucentBackground);
_ui->spinnerQuickWidget->setClearColor(Qt::transparent);
_ui->spinnerQuickWidget->setSource(QUrl::fromLocalFile(":/update_spinner.qml"));
// can't add the widget to the page directly in the .ui file since the constructor needs parameters
_ui->updaterPage->layout()->addWidget(_updater);
// make sure the QDialog resizes with the spoiler
layout()->setSizeConstraint(QLayout::SetFixedSize);
// make sure that when the embedded dialog closes, the parent dialog closes, too
connect(_updater, &QDialog::finished, this, &QDialog::done);
// set up updater
_updater->enableRunUpdatedAppImageButton(false);
connect(
_updater,
&QtUpdater::newStatusMessage,
this,
[this](const std::string& newMessage) {
if (_updaterStatusMessages.tellp() > 0)
_updaterStatusMessages << std::endl;
_updaterStatusMessages << newMessage;
}
);
connect(this, &UpdateDialog::updateCheckFinished, this, [this](int updateCheckResult) {
switch (updateCheckResult) {
case 1:
clearStatusMessages();
_ui->stackedWidget->setCurrentWidget(_ui->updaterPage);
_updater->update();
return;
case 0: {
_ui->errorTitleLabel->setText(tr("No updates found"));
_ui->errorMessageLabel->setText(tr("Could not find updates for AppImage %1").arg(_pathToAppImage));
connect(_ui->errorButtonBox, &QDialogButtonBox::clicked, this, &QDialog::accept);
break;
}
case -1: {
_ui->errorTitleLabel->setText(tr("No update information found"));
_ui->errorMessageLabel->setText(
tr("Could not find update information in AppImage:\n%1"
"\n"
"\n"
"The AppImage doesn't support updating. Please ask the authors to embed "
"update information to allow for easy updating."
).arg(_pathToAppImage)
);
connect(_ui->errorButtonBox, &QDialogButtonBox::clicked, this, &QDialog::reject);
break;
}
default: {
_ui->errorTitleLabel->setText(tr("Update check failed"));
_ui->errorMessageLabel->setText(tr("Failed to check for updates:\n%1").arg(_pathToAppImage));
connect(_ui->errorButtonBox, &QDialogButtonBox::clicked, this, &QDialog::reject);
break;
}
}
_ui->stackedWidget->setCurrentWidget(_ui->errorPage);
qCritical() << statusMessages();
});
asyncCheckForUpdate();
}
~UpdateDialog() override {
// TODO: parenting in libappimageupdate
delete _updater;
}
QString statusMessages() const {
return QString::fromStdString(_updaterStatusMessages.str());
}
void clearStatusMessages() {
_updaterStatusMessages.clear();
}
void asyncCheckForUpdate() {
// TODO: implement an async update check in libappimageupdate
auto* thread = QThread::create([this]() {
auto updateCheckResult = _updater->checkForUpdates();
emit updateCheckFinished(updateCheckResult);
});
thread->start();
}
signals:
void updateCheckFinished(int checkResult);
private:
const QString _pathToAppImage;
Ui::UpdateDialog* _ui;
QtUpdater *_updater;
std::ostringstream _updaterStatusMessages;
};
#include "update_main.moc"
int main(int argc, char** argv) {
QCommandLineParser parser;
parser.setApplicationDescription(QObject::tr("Updates AppImages after desktop integration, for use by Linux distributions"));
QApplication app(argc, argv);
QApplication::setApplicationDisplayName(QObject::tr("AppImageLauncher update", "update helper app name"));
QApplication::setWindowIcon(QIcon(":/AppImageLauncher.svg"));
std::ostringstream version;
version << "version " << APPIMAGELAUNCHER_VERSION << " "
<< "(git commit " << APPIMAGELAUNCHER_GIT_COMMIT << "), built on "
<< APPIMAGELAUNCHER_BUILD_DATE;
QApplication::setApplicationVersion(QString::fromStdString(version.str()));
// install translations
TranslationManager translationManager(app);
parser.addHelpOption();
parser.addVersionOption();
parser.process(app);
parser.addPositionalArgument("path", "Path to AppImage", "<path>");
if (parser.positionalArguments().empty()) {
parser.showHelp(1);
}
const auto pathToAppImage = parser.positionalArguments().first();
auto criticalUpdaterError = [](const QString& message) {
QMessageBox::critical(nullptr, "Error", message);
};
if (!QFile(pathToAppImage).exists()) {
criticalUpdaterError(QString::fromStdString(QObject::tr("Error: no such file or directory: %1").arg(pathToAppImage).toStdString()));
return 1;
}
const auto type = appimage_get_type(pathToAppImage.toStdString().c_str(), false);
if (type <= 0 || type > 2) {
criticalUpdaterError(QObject::tr("Not an AppImage: %1").arg(pathToAppImage));
return 1;
}
auto dialog = new UpdateDialog(pathToAppImage);
dialog->show();
return QApplication::exec();
}
07070100000048000081a400000000000000000000000168cf6940000000dc000000000000000000000000000000000000001a00000000src/ui/update_spinner.qmlimport QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
ColumnLayout {
BusyIndicator {
running: true
width: 64
height: 64
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
}
07070100000049000041ed00000000000000000000000168cf694000000000000000000000000000000000000000000000000700000000src/ui0707010000004a000041ed00000000000000000000000168cf694000000000000000000000000000000000000000000000000400000000src07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000b00000000TRAILER!!!