diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml new file mode 100644 index 0000000..d6401b0 --- /dev/null +++ b/.github/workflows/packages.yml @@ -0,0 +1,359 @@ +# Builds simd and libsimapi as .deb, .rpm, AppImage and Flatpak, and attaches +# them to a release when the run is for a tag. +# +# Built in containers on ordinary GitHub-hosted runners rather than against +# self-hosted runner labels, so nothing queues waiting for a machine that may +# not be online. +name: Packages + +on: + push: + tags: [ "*" ] + pull_request: + workflow_dispatch: {} + +jobs: + debs: + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + image: ubuntu:latest + - os: debian-latest + image: debian:testing + - os: debian-stable + image: debian:stable-slim + runs-on: ubuntu-latest + container: + image: ${{ matrix.image }} + permissions: + contents: write + steps: + # Before checkout: these images ship no git, so actions/checkout would + # silently fall back to a tarball download. + - name: Install build dependencies + run: | + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + ca-certificates git build-essential cmake pkg-config dpkg-dev \ + libuv1-dev libargtable2-dev libconfig-dev libyder-dev \ + libxdg-basedir-dev libprocps-dev || \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + ca-certificates git build-essential cmake pkg-config dpkg-dev \ + libuv1-dev libargtable2-dev libconfig-dev libyder-dev \ + libxdg-basedir-dev libproc2-dev + + - uses: actions/checkout@v4 + + - name: Determine package version + id: pkgver + shell: bash + run: | + # The control files carry a hardcoded `Version: 1` that nobody + # maintains. A Debian version must start with a digit, so a branch + # build or a non-release tag falls back to 0.0.0 rather than making + # dpkg-deb fail outright. + v="" + [ "$GITHUB_REF_TYPE" = tag ] && v="${GITHUB_REF_NAME#v}" + case "$v" in [0-9]*) ;; *) v="0.0.0" ;; esac + echo "version=$v" >> "$GITHUB_OUTPUT" + echo "packaging as version $v" + + - name: Build + run: | + # SYSTEMD_USER_UNIT_DIR and SIMD_CONFIG_DIR default under $ENV{HOME}; + # a package must never write into a user's home. Both are already + # CACHE PATH variables upstream, so redirecting needs no patch. + cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr \ + -DBUILD_SIMD=ON \ + -DSYSTEMD_USER_UNIT_DIR=/usr/lib/systemd/user \ + -DSIMD_CONFIG_DIR=/usr/share/simd + cmake --build build -j"$(nproc)" + + - name: Assemble the two packages + shell: bash + run: | + set -eux + ver='${{ steps.pkgver.outputs.version }}' + ctl=tools/distro/debian/dpkg/${{ matrix.os }} + + # libsimapi: the shared library and its headers. Kept a separate + # package because simd is not its only possible consumer. + rm -rf lib_root && mkdir -p lib_root/DEBIAN lib_root/usr/lib lib_root/usr/include lib_root/usr/share/pkgconfig + cp -P build/libsimapi.so* lib_root/usr/lib/ + cp simapi/simmapper.h simapi/simapi.h simapi/simdata.h lib_root/usr/include/ + cp build/simapi.pc lib_root/usr/share/pkgconfig/ + sed "s/^Version:.*/Version: $ver/" "$ctl/simapi_control" > lib_root/DEBIAN/control + dpkg-deb --build lib_root "libsimapi-${{ matrix.os }}.deb" + + # simd: the daemon, plus the packaged unit and an example config. + rm -rf simd_root && mkdir -p simd_root/DEBIAN simd_root/usr/bin + cp build/simd/simd simd_root/usr/bin/simd + install -Dm644 tools/distro/simd.service simd_root/usr/lib/systemd/user/simd.service + install -Dm644 simd/conf/simd.config simd_root/usr/share/simd/simd.config + sed "s/^Version:.*/Version: $ver/" "$ctl/simd_control" > simd_root/DEBIAN/control + dpkg-deb --build simd_root "simd-${{ matrix.os }}.deb" + + echo "--- control as packaged ---" + dpkg-deb --field "simd-${{ matrix.os }}.deb" Package Version Depends + + - uses: actions/upload-artifact@v4 + with: + name: debs-${{ matrix.os }} + path: "*.deb" + + - name: Release the packages + if: github.ref_type == 'tag' + uses: softprops/action-gh-release@v1 + with: + files: "*.deb" + + rpms: + strategy: + fail-fast: false + matrix: + os: [fedora-43, fedora-44] + include: + - os: fedora-43 + image: fedora:43 + - os: fedora-44 + image: fedora:44 + runs-on: ubuntu-latest + container: + image: ${{ matrix.image }} + permissions: + contents: write + steps: + - name: Install build dependencies + run: | + dnf install -y --setopt=install_weak_deps=False \ + git rpm-build rpmdevtools gcc gcc-c++ cmake pkgconf-pkg-config \ + libuv-devel argtable-devel libconfig-devel \ + libxdg-basedir-devel procps-ng-devel + + # yder, and orcania beneath it, are what simd logs through, and Fedora + # packages neither -- `dnf install yder-devel` fails outright with "No + # match for argument" on both 43 and 44, while Debian and Ubuntu ship + # libyder-dev. Built here as *static* libraries on purpose: linked into + # simd they leave the rpm with no runtime dependency Fedora cannot + # satisfy, so the spec's Requires: stays as upstream wrote it. + - name: Build vendored orcania and yder (static) + run: | + set -eux + # BUILD_SHARED=OFF as well as BUILD_STATIC=ON: both default to + # shared-only, and a libyder.so left in the prefix is picked over the + # archive, putting back a runtime dependency no Fedora repo can meet. + # + # -Wno-error via CMAKE_C_FLAGS_RELEASE, not CMAKE_C_FLAGS: both + # projects append their own -Wall -Werror to CMAKE_C_FLAGS, so + # setting it is overridden. The _RELEASE flags land after it, where + # the last of -Werror/-Wno-error wins. orcania 2.3.3 assigns the + # result of strstr() on a const pointer to a char*, which current + # GCC rejects as -Werror=discarded-qualifiers. + cflags='-O2 -DNDEBUG -Wno-error' + + git clone --depth 1 --branch v2.3.3 https://github.com/babelouest/orcania /tmp/orcania + cmake -S /tmp/orcania -B /tmp/orcania/build \ + -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_C_FLAGS_RELEASE="$cflags" \ + -DBUILD_SHARED=OFF -DBUILD_STATIC=ON -DBUILD_ORCANIA_TESTING=OFF + cmake --build /tmp/orcania/build --target install + + git clone --depth 1 --branch v1.4.20 https://github.com/babelouest/yder /tmp/yder + # Journald would pull in systemd-devel for a backend simd never + # selects; its config offers file and syslog. + cmake -S /tmp/yder -B /tmp/yder/build \ + -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_C_FLAGS_RELEASE="$cflags" \ + -DBUILD_SHARED=OFF -DBUILD_STATIC=ON -DWITH_JOURNALD=OFF \ + -DBUILD_YDER_TESTING=OFF + cmake --build /tmp/yder/build --target install + + # Fail here rather than shipping a broken rpm: nothing later in this + # job would notice a dynamically linked simd. + test -f /usr/lib64/libyder.a || test -f /usr/lib/libyder.a + + - uses: actions/checkout@v4 + + - name: Determine package version + id: pkgver + shell: bash + run: | + v="" + [ "$GITHUB_REF_TYPE" = tag ] && v="${GITHUB_REF_NAME#v}" + case "$v" in [0-9]*) ;; *) v="0.0.0" ;; esac + echo "version=$v" >> "$GITHUB_OUTPUT" + + - name: Build both rpms + shell: bash + run: | + set -eux + rpmdev-setuptree + # Stage this checkout as the source, so an rpm's contents are the + # commit it was built from rather than whatever the default branch + # holds -- see the %prep comment in each spec. + rm -rf ~/rpmbuild/SOURCES/simapi + cp -r "$GITHUB_WORKSPACE" ~/rpmbuild/SOURCES/simapi + rm -rf ~/rpmbuild/SOURCES/simapi/.github + + # The specs' hardcoded `Version: 0.0.5` is stale; stamped from the + # tag here so the file stays a working default for a local rpmbuild. + for spec in simapi simd; do + rpmbuild -ba --define "_version ${{ steps.pkgver.outputs.version }}" \ + --define "version ${{ steps.pkgver.outputs.version }}" \ + tools/distro/fedora/rpm/$spec.spec + done + + # *-[0-9]*, not *: rpmbuild also emits debuginfo and debugsource. + cp ~/rpmbuild/RPMS/x86_64/simd-[0-9]*.rpm "simd-${{ matrix.os }}.rpm" + cp ~/rpmbuild/RPMS/x86_64/libsimapi-[0-9]*.rpm "libsimapi-${{ matrix.os }}.rpm" + rpm -qp --requires "simd-${{ matrix.os }}.rpm" + + - uses: actions/upload-artifact@v4 + with: + name: rpms-${{ matrix.os }} + path: "*.rpm" + + - name: Release the packages + if: github.ref_type == 'tag' + uses: softprops/action-gh-release@v1 + with: + files: "*.rpm" + + appimage: + # Oldest base that actually builds this, since an AppImage is only as + # portable as the oldest glibc it links against. 22.04 does not: it ships + # libprocps rather than libproc2, so getpid.c fails on a missing + # , and its GCC predates the C23 fixed-underlying-type + # enums in include/F12018/*.h ("expected identifier or '(' before ':'"). + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + build-essential cmake pkg-config file wget \ + libuv1-dev libargtable2-dev libconfig-dev libyder-dev \ + libxdg-basedir-dev libproc2-dev + + - name: Determine version + id: pkgver + shell: bash + run: | + v="" + [ "$GITHUB_REF_TYPE" = tag ] && v="${GITHUB_REF_NAME#v}" + case "$v" in [0-9]*) ;; *) v="0.0.0" ;; esac + echo "version=$v" >> "$GITHUB_OUTPUT" + + - name: Build and stage an AppDir + run: | + set -eux + # CMAKE_INSTALL_LIBDIR pinned: GNUInstallDirs picks the multiarch + # triplet on Ubuntu, so the library landed in + # AppDir/usr/lib/x86_64-linux-gnu while everything downstream looked + # in AppDir/usr/lib. + cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr \ + -DBUILD_SIMD=ON -DCMAKE_INSTALL_LIBDIR=lib \ + -DSYSTEMD_USER_UNIT_DIR=/usr/lib/systemd/user \ + -DSIMD_CONFIG_DIR=/usr/share/simd + cmake --build build -j"$(nproc)" + DESTDIR="$PWD/AppDir" cmake --install build + # An AppImage cannot install a systemd unit onto the host, and the + # daemon is started directly rather than supervised, so it carries + # the example config but not the unit. + rm -f AppDir/usr/lib/systemd/user/simd.service + install -Dm644 tools/distro/flatpak/io.github.spacefreak18.simd.desktop \ + AppDir/usr/share/applications/io.github.spacefreak18.simd.desktop + # linuxdeploy takes the icon's *name* from its filename and matches + # it against the desktop file's Icon= entry, so simd-128.png would + # be deployed as "simd-128" and the entry naming + # io.github.spacefreak18.simd finds nothing: "Could not find + # suitable icon for Icon entry". + cp tools/distro/simd-128.png io.github.spacefreak18.simd.png + install -Dm644 LICENSE.rst AppDir/usr/share/doc/simd/LICENSE.rst + + - name: Package the AppImage + env: + # These tools are themselves AppImages and there is no FUSE on the + # runner to mount them with. + APPIMAGE_EXTRACT_AND_RUN: 1 + run: | + set -eux + wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage + chmod +x linuxdeploy-x86_64.AppImage + export OUTPUT="simd-${{ steps.pkgver.outputs.version }}-x86_64.AppImage" + # simd links libsimapi.so.1, which is inside the AppDir and nowhere + # on the system, so linuxdeploy's ldd-based resolution reported + # "Could not find dependency: libsimapi.so.1" and refused to deploy. + export LD_LIBRARY_PATH="$PWD/AppDir/usr/lib:${LD_LIBRARY_PATH:-}" + ./linuxdeploy-x86_64.AppImage --appdir AppDir \ + --desktop-file AppDir/usr/share/applications/io.github.spacefreak18.simd.desktop \ + --icon-file io.github.spacefreak18.simd.png \ + --output appimage + + - uses: actions/upload-artifact@v4 + with: + name: appimage + path: "simd-*-x86_64.AppImage" + + - name: Release the AppImage + if: github.ref_type == 'tag' + uses: softprops/action-gh-release@v1 + with: + files: "simd-*-x86_64.AppImage" + + flatpak: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - name: Install flatpak-builder + run: | + sudo apt-get update + # Recommends left enabled deliberately: flatpak-builder shells out to + # tar's decompressors, eu-strip and patch, which Debian and Ubuntu + # list as Recommends rather than Depends. With --no-install-recommends + # it installs, builds several modules, then fails on whichever helper + # the next module needs. + sudo apt-get install -y flatpak flatpak-builder xz-utils bzip2 elfutils + flatpak remote-add --if-not-exists --user flathub https://flathub.org/repo/flathub.flatpakrepo + flatpak install -y --user --noninteractive flathub \ + org.freedesktop.Platform//25.08 org.freedesktop.Sdk//25.08 + + - name: Build the bundle + working-directory: tools/distro/flatpak + run: | + flatpak-builder --user --disable-rofiles-fuse --force-clean --repo=repo \ + build-dir io.github.spacefreak18.simd.yml + flatpak build-bundle repo simd.flatpak io.github.spacefreak18.simd + + # Install and run it. A Flatpak that builds is not a Flatpak that works: + # libuv and libconfig installed to /app/lib64, which is not on the + # runtime's library search path, so the bundle built and packaged + # cleanly and then died on startup with + # simd: error while loading shared libraries: libuv.so.1 + # Nothing in this job had ever executed the binary. --version is enough + # to prove every shared library resolves. + - name: Smoke-test the bundle + working-directory: tools/distro/flatpak + run: | + flatpak install -y --user --bundle simd.flatpak + flatpak run io.github.spacefreak18.simd --version + + - uses: actions/upload-artifact@v4 + with: + name: simd.flatpak + path: tools/distro/flatpak/simd.flatpak + + - name: Release the bundle + if: github.ref_type == 'tag' + uses: softprops/action-gh-release@v1 + with: + files: tools/distro/flatpak/simd.flatpak diff --git a/CMakeLists.txt b/CMakeLists.txt index d3e0678..a4975b7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,9 +20,13 @@ pkg_check_modules(LIBPROC2 libproc2) if (LIBPROCPS_FOUND) #add_compile_definitions(USE_OLD_PID_VAL=0) elseif (LIBPROC2_FOUND) - if (LIBPROC2_VERSION VERSION_GREATER_EQUAL "4.0.5") - #add_compile_definitions(USE_OLD_PID_VAL=0) - else() + # Some distro packagings -- the freedesktop Flatpak SDK among them -- ship + # a libproc2.pc whose Version field is the literal string UNKNOWN. Compared + # against that, VERSION_GREATER_EQUAL is always false, so this fell through + # to the pre-4.0.5 four-argument PIDS_VAL API and failed to compile against + # a current libproc2 whose header has long since moved to three. Take the + # old-API branch only when the version really parses and really is old. + if (LIBPROC2_VERSION MATCHES "^[0-9]" AND LIBPROC2_VERSION VERSION_LESS "4.0.5") add_compile_definitions(USE_OLD_PID_VAL) endif() else() diff --git a/simapi/getpid.c b/simapi/getpid.c index c88ac84..5b183cb 100644 --- a/simapi/getpid.c +++ b/simapi/getpid.c @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -38,6 +39,11 @@ static int isMatch(const char* possibleMatch, const char* checkAgainst) int is_pid_running(pid_t pid) { + if (simapi_in_flatpak()) + { + return simapi_host_pid_alive(pid); + } + if (pid <= 0) { return 0; @@ -114,16 +120,31 @@ char* getEnvValueForPid(pid_t pid, const char* envName) sprintf( &path[6], "%d/environ", pid ); - envFile = fopen(path, "r"); - if ( envFile == NULL ) + /* The pid came from the host's process table, so its /proc entry exists + * only out there -- see simapi_host_capture(). */ + if ( simapi_in_flatpak() ) { -// errno = ESRCH; - return NULL; + char hostcmd[96]; + snprintf(hostcmd, sizeof(hostcmd), "cat /proc/%d/environ", (int) pid); + buf = simapi_host_capture(hostcmd, &maxIdx); + if ( buf == NULL ) + { + return NULL; + } } + else + { + envFile = fopen(path, "r"); + if ( envFile == NULL ) + { +// errno = ESRCH; + return NULL; + } - maxIdx = readData(&buf, envFile); + maxIdx = readData(&buf, envFile); - fclose(envFile); + fclose(envFile); + } envNameLen = strlen(envName); cur = buf; @@ -171,9 +192,249 @@ int check_if_number (char* str) } + +/* ------------------------------------------------------------------------ + * Looking at the host from inside a Flatpak sandbox. + * + * A sandbox gets its own PID namespace -- measured on a real install, 5 pids + * visible inside against 497 on the host -- so the /proc walk below finds only + * the sandbox itself and no sim is ever detected. Flatpak has no option to + * share the host's namespace (--allow=devel and --filesystem=host both change + * nothing), leaving `flatpak-spawn --host`, which asks the session helper to + * run a command outside. That needs --talk-name=org.freedesktop.Flatpak in the + * manifest. + * + * The output comes back through a FILE, not a pipe. A popen()'d + * `flatpak-spawn --host ...` hands the pipe's write end to the session helper, + * and a copy of it outlives every process the sandbox can see: reading to EOF + * never returns. Measured -- simd sat in anon_pipe_read with no children left + * and no further log line, forever. So the host writes to a file under + * $XDG_RUNTIME_DIR/app/$FLATPAK_ID, the one directory mounted at the same path + * on both sides, and renames it into place; the sandbox then reads an ordinary + * file where EOF means what it says. The rename is what makes "the file + * exists" mean "the output is complete". + * + * None of this is reached outside a sandbox: every caller keeps its original + * code path, so native builds are byte-for-byte unaffected. + * --------------------------------------------------------------------- */ +int simapi_in_flatpak(void) +{ + static int cached = -1; + if (cached < 0) + { + cached = (access("/.flatpak-info", F_OK) == 0) ? 1 : 0; + } + return cached; +} + +/* Visible under the same path inside the sandbox and out. */ +static const char* host_share_dir(void) +{ + static char dir[256]; + if (dir[0] == '\0') + { + const char* runtime = getenv("XDG_RUNTIME_DIR"); + const char* app_id = getenv("FLATPAK_ID"); + if (runtime != NULL && app_id != NULL) + { + snprintf(dir, sizeof(dir), "%s/app/%s", runtime, app_id); + } + else + { + snprintf(dir, sizeof(dir), "/tmp"); + } + } + return dir; +} + +/* Runs `hostcmd` on the host and returns its stdout, or NULL. Caller frees. */ +char* simapi_host_capture(const char* hostcmd, size_t* out_len) +{ + static unsigned long seq = 0; + char out[320]; + char cmd[1024]; + + if (out_len != NULL) + { + *out_len = 0; + } + + snprintf(out, sizeof(out), "%s/simapi-host-%d-%lu", host_share_dir(), (int) getpid(), seq++); + + /* Written to .part and renamed, so the file only appears complete. + * The exit status is deliberately ignored: simd runs a libuv loop whose + * SIGCHLD handling reaps children this call did not spawn, which makes + * system()'s own waitpid fail and lose the status. */ + snprintf(cmd, sizeof(cmd), + /* hostcmd is grouped: it may itself be a sequence, and without + * the braces the redirect would bind to its last command only -- + * which silently dropped half the process table. */ + "flatpak-spawn --host sh -c '{ %s ; } > %s.part 2>/dev/null; mv %s.part %s' >/dev/null 2>&1", + hostcmd, out, out, out); + (void) system(cmd); + + /* system() normally returns only once the command is done; the wait can + * still be lost to that same reaping, so give the rename a moment. */ + FILE* f = NULL; + for (int waited = 0; waited < 2000; waited += 20) + { + f = fopen(out, "r"); + if (f != NULL) + { + break; + } + usleep(20 * 1000); + } + + if (f == NULL) + { + return NULL; + } + + size_t cap = 8192; + size_t len = 0; + char* buf = malloc(cap); + + if (buf != NULL) + { + for (;;) + { + size_t n = fread(buf + len, 1, cap - len - 1, f); + len += n; + if (len + 1 < cap) + { + break; /* short read on a real file means EOF */ + } + cap *= 2; + char* grown = realloc(buf, cap); + if (grown == NULL) + { + break; + } + buf = grown; + } + buf[len] = '\0'; + } + + fclose(f); + unlink(out); + + if (out_len != NULL) + { + *out_len = len; + } + return buf; +} + +/* Is a pid in the *host's* namespace alive? kill(2) would resolve the number + * against the sandbox's own processes -- usually nothing, occasionally the + * wrong one. */ +int simapi_host_pid_alive(pid_t pid) +{ + char hostcmd[96]; + snprintf(hostcmd, sizeof(hostcmd), "kill -0 %d 2>/dev/null && echo alive", (int) pid); + + char* out = simapi_host_capture(hostcmd, NULL); + int alive = (out != NULL && strncmp(out, "alive", 5) == 0); + free(out); + return alive; +} + +/* Does a path exist on the *host*? For paths that are the host's by nature -- + * a Proton binary under STEAM_COMPAT_TOOL_PATHS, say -- since the sandbox's + * own filesystem knows nothing about them. Checking locally is how the bridge + * launch failed silently: Proton was never found, so the fork never happened + * and nothing was logged above debug level. */ +int simapi_host_file_exists(const char* path) +{ + char hostcmd[1024]; + snprintf(hostcmd, sizeof(hostcmd), "test -e '%s' && echo yes", path); + + char* out = simapi_host_capture(hostcmd, NULL); + int exists = (out != NULL && strncmp(out, "yes", 3) == 0); + free(out); + return exists; +} + +static struct SimProcessInfo pidof_host(char* pname[], int num) +{ + struct SimProcessInfo p; + p.pid = -1; + p.pos = -1; + + /* Two ps runs, each line tagged, because a single `ps -o pid=,comm=,args=` + * cannot be parsed: comm may contain spaces. Wine names Assetto Corsa's + * process "AC: main thread", which a naive scanf read as comm="AC:" and + * argv[0]="main" -- nothing matched "acs.exe", simapi never set simstatus, + * and simd mapped no telemetry at all while the game ran. Splitting the + * two fields into their own lines keeps each one unambiguous: "A + * " gives argv[0] as the first token after the pid, and + * "C " gives comm as the whole rest of the line, which is + * the same pair libproc2 hands the native path below. */ + char* table = simapi_host_capture( + "ps -A -o pid=,args= | sed \"s/^/A /\"; ps -A -o pid=,comm= | sed \"s/^/C /\"", + NULL); + if (table == NULL) + { + return p; + } + + char* saveptr = NULL; + for (char* line = strtok_r(table, "\n", &saveptr); + line != NULL; + line = strtok_r(NULL, "\n", &saveptr)) + { + char kind = line[0]; + if (kind != 'A' && kind != 'C') + { + continue; + } + + int pid = 0; + int consumed = 0; + if (sscanf(line + 1, " %d %n", &pid, &consumed) < 1 || consumed == 0) + { + continue; + } + + char* field = line + 1 + consumed; + if (kind == 'A') + { + /* argv[0] only. The whole of `ps -o args=` would be looser than + * libproc2's PIDS_CMDLINE_V first element: a shell whose arguments + * merely mention a sim's exe -- a Steam launch command, say -- + * would match, and simd would track that shell as the game. */ + char* end = strchr(field, ' '); + if (end != NULL) + { + *end = '\0'; + } + } + + for (int i = 0; pname[i] != NULL && i < num; i++) + { + if (strcasestr(field, pname[i]) != NULL) + { + p.pid = pid; + p.pos = i; + free(table); + return p; + } + } + } + + free(table); + return p; +} + struct SimProcessInfo pidof (char* pname[], int num) { + if (simapi_in_flatpak()) + { + return pidof_host(pname, num); + } + struct SimProcessInfo p; p.pid = -1; p.pos = -1; diff --git a/simapi/simapi.h b/simapi/simapi.h index 3d53b12..4667bbc 100644 --- a/simapi/simapi.h +++ b/simapi/simapi.h @@ -113,6 +113,10 @@ struct SimProcessInfo void simapi_set_faux_siminfo(SimInfo* si); +int simapi_in_flatpak(void); +char* simapi_host_capture(const char* hostcmd, size_t* out_len); +int simapi_host_pid_alive(pid_t pid); +int simapi_host_file_exists(const char* path); int is_pid_running(pid_t pid); struct SimProcessInfo get_process_match(char* pidstrings[], int num); char* getEnvValueForPid(pid_t pid, const char* envName); diff --git a/simd/CMakeLists.txt b/simd/CMakeLists.txt index 2036145..a31f0e6 100644 --- a/simd/CMakeLists.txt +++ b/simd/CMakeLists.txt @@ -32,6 +32,19 @@ endif() add_executable(simd simd.c parameters.c confighelper.c dirhelper.c poke.c ../simmap/mapsimdata.c) target_link_libraries(simd m uv yder ${ARGTABLE_LIBS} config simapi) +# yder calls into orcania -- o_malloc, o_free, o_strdup, o_strnullempty, +# split_string. A shared libyder.so records that dependency and the linker +# follows it, which is why this is not needed on Debian or Ubuntu. A static +# libyder.a records nothing, so all of those come back undefined unless +# orcania is named explicitly, and after yder, which is the order a static +# link needs. Distributions without a yder package -- Fedora among them -- +# have to vendor it, and vendoring it static is what keeps the resulting +# package free of a dependency those distributions cannot satisfy. +find_library(ORCANIA_LIBRARY orcania) +if(ORCANIA_LIBRARY) + target_link_libraries(simd ${ORCANIA_LIBRARY}) +endif() + target_include_directories(simd PRIVATE ${ARGTABLE_INCLUDE_DIR}) # User-level installation diff --git a/simd/simd.c b/simd/simd.c index f422f76..320ad7a 100644 --- a/simd/simd.c +++ b/simd/simd.c @@ -350,6 +350,21 @@ int startudp(int port) return err; } +/* Paths handed to the bridge launch are the host's whenever this is a Flatpak + * build; everywhere else this is the ordinary local check. */ +static bool bridge_file_exists(const char* path) +{ + if (path == NULL) + { + return false; + } + if (simapi_in_flatpak()) + { + return simapi_host_file_exists(path) ? true : false; + } + return does_file_exist(path); +} + int is_pid_running(pid_t pid) { if (pid <= 0) @@ -357,6 +372,14 @@ int is_pid_running(pid_t pid) return 0; } + /* Sandboxed, the pids simd tracks belong to the host's namespace, where + * kill(2) cannot reach them. Shared with simapi so both halves agree on + * how the question is asked. */ + if (simapi_in_flatpak()) + { + return simapi_host_pid_alive(pid); + } + // send signal 0 (no actual signal) if (kill(pid, 0) == 0) { @@ -395,7 +418,8 @@ void bridgeclosecallback(uv_timer_t* handle) if(simds.notify == true) { char cmd[512]; - snprintf(cmd, sizeof(cmd), "notify-send -t 3000 \"%s\" \"game stopped\"", "simd"); + snprintf(cmd, sizeof(cmd), "%snotify-send -t 3000 \"%s\" \"game stopped\"", + simapi_in_flatpak() ? "flatpak-spawn --host " : "", "simd"); system(cmd); } @@ -519,7 +543,10 @@ void gamefindcallback(uv_timer_t* handle) { char* pathcheck1 = NULL; asprintf(&pathcheck1, "%s/dist/bin/wine", token); - if(does_file_exist(pathcheck1) == true) + /* Proton lives on the host, so ask the host. Locally this + * always answered false under Flatpak and the bridge was + * never launched. */ + if(bridge_file_exists(pathcheck1) == true) { wineexe = strdup(pathcheck1); } @@ -531,7 +558,7 @@ void gamefindcallback(uv_timer_t* handle) if(wineexe == NULL) { asprintf(&pathcheck1, "%s/files/bin/wine", token); - if(does_file_exist(pathcheck1) == true) + if(bridge_file_exists(pathcheck1) == true) { wineexe = strdup(pathcheck1); } @@ -598,13 +625,41 @@ void gamefindcallback(uv_timer_t* handle) close(devnull); } - if(env_simd_wrap_exe == NULL) + char* target = (env_simd_wrap_exe == NULL) ? wineexe : env_simd_wrap_exe; + + if(simapi_in_flatpak()) { - ret = execve(wineexe, newargv, newenviron); + /* Proton, its prefix and the bridge exe are all on the + * host; execve here would look for them inside the + * runtime. --env carries the environment across, since + * flatpak-spawn does not pass this one through, and + * --watch-bus ties the host process's lifetime to this + * one so the existing SIGTERM teardown still ends it. */ + char* hostargv[16]; + char envopts[4][512]; + int n = 0; + int e = 0; + + hostargv[n++] = "flatpak-spawn"; + hostargv[n++] = "--host"; + hostargv[n++] = "--watch-bus"; + for(int i = 0; newenviron[i] != NULL && e < 4; i++) + { + snprintf(envopts[e], sizeof(envopts[e]), "--env=%s", newenviron[i]); + hostargv[n++] = envopts[e]; + e++; + } + for(int i = 0; newargv[i] != NULL && n < 15; i++) + { + hostargv[n++] = newargv[i]; + } + hostargv[n] = NULL; + + ret = execvp("flatpak-spawn", hostargv); } else { - ret = execve(env_simd_wrap_exe, newargv, newenviron); + ret = execve(target, newargv, newenviron); } _exit(127); } @@ -642,7 +697,8 @@ void gamefindcallback(uv_timer_t* handle) { char cmd[512]; const char* gamename = simapi_gametofullstr(sim); - snprintf(cmd, sizeof(cmd), "notify-send -t 3000 \"%s\" \"Detected %s (%i)\"", "simd", gamename, sim); + snprintf(cmd, sizeof(cmd), "%snotify-send -t 3000 \"%s\" \"Detected %s (%i)\"", + simapi_in_flatpak() ? "flatpak-spawn --host " : "", "simd", gamename, sim); system(cmd); } } diff --git a/stage/usr/bin/simd b/stage/usr/bin/simd new file mode 100755 index 0000000..c2a3ced Binary files /dev/null and b/stage/usr/bin/simd differ diff --git a/stage/usr/include/simapi.h b/stage/usr/include/simapi.h new file mode 100644 index 0000000..4667bbc --- /dev/null +++ b/stage/usr/include/simapi.h @@ -0,0 +1,124 @@ +#ifndef _SIMMAPI_H +#define _SIMMAPI_H + +#include +#include + +#define SIMAPI_VERSION 1 + +typedef void (*func_ptr_t)(char* message); +//func_ptr_t logfunc; + +void simapi_set_log_info(func_ptr_t logfunc); +void simapi_set_log_debug(func_ptr_t logfunc); +void simapi_set_log_trace(func_ptr_t logfunc); + + +typedef enum +{ + SIMAPI_LOGLEVEL_INFO = 0, + SIMAPI_LOGLEVEL_DEBUG = 1, + SIMAPI_LOGLEVEL_TRACE = 2, +} +SIMAPI_LOGLEVEL; + +typedef enum +{ + SIMULATORAPI_SIMAPI_TEST = 0, + SIMULATORAPI_ASSETTO_CORSA = 1, + SIMULATORAPI_RFACTOR2 = 2, + SIMULATORAPI_PROJECTCARS2 = 3, + SIMULATORAPI_SCSTRUCKSIM2 = 4, + SIMULATORAPI_OUTSIMOUTGAUGE = 5, + SIMULATORAPI_DIRT_RALLY_2 = 6, + SIMULATORAPI_F1_2018 = 7, + SIMULATORAPI_RACE_ROOM = 8, + SIMULATORAPI_FORZA = 9, + SIMULATORAPI_LMU = 10, + SIMULATORAPI_WRECKFEST2 = 11, + SIMULATORAPI_RICHARD_BURNS_RALLY = 12, +} +SimulatorAPI; + +typedef enum +{ + SIMULATOREXE_SIMAPI_TEST_NONE = 0, + SIMULATOREXE_ASSETTO_CORSA = 244210, //ac + SIMULATOREXE_RFACTOR2 = 365960, //rf2 + SIMULATOREXE_AUTOMOBILISTA2 = 1066890, //ams2 + SIMULATOREXE_AUTOMOBILISTA2_DEMO = 1786210, //ams2demo + SIMULATOREXE_EUROTRUCKS2 = 227300, //et2 + SIMULATOREXE_AMERICANTRUCKS = 270880, //at + SIMULATOREXE_ASSETTO_CORSA_COMPETIZIONE = 805550, //acc + SIMULATOREXE_ASSETTO_CORSA_EVO = 3058630, //ace + SIMULATOREXE_ASSETTO_CORSA_RALLY = 3917090, //acr + SIMULATOREXE_LEMANS_ULTIMATE = 2399420, //lmu + SIMULATOREXE_BEAMNG = 284160, //beamng + SIMULATOREXE_LIVE_FOR_SPEED = 0000001, //lfs + SIMULATOREXE_DIRT_RALLY_2 = 690790, //dr2 + SIMULATOREXE_F1_2022 = 1692250, //f122 + SIMULATOREXE_RACE_ROOM = 211500, //r3e + SIMULATOREXE_FORZA_HORIZON_5 = 1551360, //fh5 + SIMULATOREXE_FORZA_HORIZON_6 = 2483190, //fh6 + SIMULATOREXE_WRECKFEST2 = 1203190, //wf2 + SIMULATOREXE_RICHARD_BURNS_RALLY = 0000002, //rbr +} +SimulatorEXE; + +typedef enum +{ + SIMAPI_ERROR_NONE = 0, + SIMAPI_ERROR_UNKNOWN = 1, + SIMAPI_ERROR_INVALID_SIM = 2, + SIMAPI_ERROR_NODATA = 3, +} +SimAPIError; + +typedef enum +{ + INTEGER = 0, + DOUBLE = 1, + FLOAT = 2, + CHAR = 3, + BOOLEAN = 4, + UINT8 = 5, + UINT32 = 6, + UINT64 = 7, +} +SimDataType; + +typedef struct +{ + bool isSimOn; + bool SimUsesUDP; + bool SimSupportsBasicTelemetry; + bool SimSupportsTyreEffects; + bool SimSupportsRealtimeTelemetry; + bool SimSupportsAdvancedUI; + bool SimCalculatesTyreDiameter; + bool SimCalculatesSlipRatio; + bool SimSupportsHapticEffects; + SimulatorAPI mapapi; + SimulatorAPI simulatorapi; + SimulatorEXE simulatorexe; + pid_t pid; +} +SimInfo; + +struct SimProcessInfo +{ + int pid; + int pos; +}; + +void simapi_set_faux_siminfo(SimInfo* si); + +int simapi_in_flatpak(void); +char* simapi_host_capture(const char* hostcmd, size_t* out_len); +int simapi_host_pid_alive(pid_t pid); +int simapi_host_file_exists(const char* path); +int is_pid_running(pid_t pid); +struct SimProcessInfo get_process_match(char* pidstrings[], int num); +char* getEnvValueForPid(pid_t pid, const char* envName); + +#endif diff --git a/stage/usr/include/simdata.h b/stage/usr/include/simdata.h new file mode 100644 index 0000000..9e49efc --- /dev/null +++ b/stage/usr/include/simdata.h @@ -0,0 +1,201 @@ +#ifndef _SIMDATA_H +#define _SIMDATA_H + +#include +#include + +#define MAXCARS 128 +#define PROXCARS 6 + +typedef enum +{ + SIMAPI_STATUS_OFF = 0, + SIMAPI_STATUS_MENU = 1, + SIMAPI_STATUS_ACTIVEPLAY = 2, +} +SIMAPI_STATUS; + +typedef enum +{ + SIMAPI_FLAG_GREEN = 0, + SIMAPI_FLAG_YELLOW = 1, + SIMAPI_FLAG_RED = 2, + SIMAPI_FLAG_CHEQUERED = 3, + SIMAPI_FLAG_BLUE = 4, + SIMAPI_FLAG_WHITE = 5, + SIMAPI_FLAG_BLACK = 6, + SIMAPI_FLAG_BLACK_WHITE = 7, + SIMAPI_FLAG_BLACK_ORANGE = 8, + SIMAPI_FLAG_ORANGE = 9 +} +SIMAPI_FLAG; + +typedef enum +{ + SIMAPI_GEAR_REVERSE = 0, + SIMAPI_GEAR_NEUTRAL = 1, + SIMAPI_GEAR_FIRST = 2, + SIMAPI_GEAR_SECOND = 3, + SIMAPI_GEAR_THIRD = 4, + SIMAPI_GEAR_FOURTH = 5, + SIMAPI_GEAR_FIFTH = 6, + SIMAPI_GEAR_SIXTH = 7, + SIMAPI_GEAR_SEVENTH = 8, + SIMAPI_GEAR_EIGHT = 9 +} +SIMAPI_GEAR; + + +#pragma pack(push) +#pragma pack(4) + +typedef struct //LapTime +{ + uint32_t hours; + uint32_t minutes; + uint32_t seconds; + uint32_t fraction; +} LapTime; + +typedef struct //CarData +{ + double xpos; + double ypos; + double zpos; + double carspline; + double speed; + uint32_t pos; + uint32_t lap; + uint32_t trackpos; + LapTime lastlap; + LapTime bestlap; + // not all of these are set by each sim + bool inpit; // no matter what, if you're in the pit set this + bool inpitlane; // in pit lane, actively entering or exiting, but not stopped or in garage + bool ingarage; // not always available + bool inpitentrance; // not always available + bool inpitexit; // not always available + bool inpitstopped; // not always available + char driver[128]; + char car[128]; +} CarData; + +typedef struct //ProximityData +{ + double radius; + double theta; // in degrees + uint32_t lap; +} ProximityData; + +typedef struct //SimData +{ + uint64_t mtick; + uint64_t prev_mtick; + + uint32_t simstatus; // less than 1 is off or in menu, 2 is active + uint32_t velocity; + uint32_t rpms; + uint32_t gear; + uint32_t pulses; + uint32_t maxrpm; + uint32_t idlerpm; + uint32_t maxgears; + uint32_t altitude; + uint32_t lap; + uint32_t position; + uint32_t numlaps; + uint32_t playerlaps; + uint32_t numcars; + char gearc[3]; + + double Xvelocity; + double Yvelocity; + double Zvelocity; + + double worldXvelocity; + double worldYvelocity; + double worldZvelocity; + + double gas; + double brake; + double fuel; + double fuelcapacity; + double clutch; + double steer; + double handbrake; + + double turboboost; + double turboboostperct; + double maxturbo; + + double abs; + double brakebias; + /* Wheel angular velocity. Units: radians per second (rad/s) */ + double tyreRPS[4]; + double tyrediameter[4]; + double tyreslipratio[4]; + double tyreslipangle[4]; + double distance; + + double heading; + double pitch; + double roll; + double worldposx; + double worldposy; + double worldposz; + + double braketemp[4]; + double tyrewear[4]; + double tyretemp[4]; + double tyrepressure[4]; + + double tyrecontact0[4]; + double tyrecontact1[4]; + double tyrecontact2[4]; + + double airdensity; + double airtemp; + double tracktemp; + + double suspension[4]; + double suspvelocity[4]; + + double trackdistancearound; + double playerspline; + double trackspline; + uint32_t playertrackpos; + uint32_t tracksamples; + + LapTime lastlap; + LapTime bestlap; + LapTime currentlap; + uint32_t currentlapinseconds; + uint32_t lastlapinseconds; + uint32_t time; + LapTime sessiontime; + uint8_t session; + uint8_t sectorindex; + double sector1time; + double sector2time; + uint32_t lastsectorinms; + uint8_t courseflag; + uint8_t playerflag; + + bool lapisvalid; + + char car[128]; + char track[128]; + char driver[128]; + char tyrecompound[128]; + + CarData cars[MAXCARS]; + ProximityData pd[PROXCARS]; + + uint8_t simapi; + uint64_t simexe; + bool simon; + uint8_t simapiversion; +} SimData; + +#pragma pack(pop) +#endif diff --git a/stage/usr/include/simmapper.h b/stage/usr/include/simmapper.h new file mode 100644 index 0000000..ef9d793 --- /dev/null +++ b/stage/usr/include/simmapper.h @@ -0,0 +1,93 @@ +#ifndef _SIMMAPPER_H +#define _SIMMAPPER_H + +//#include "ac.h" +//#include "rf2.h" +//#include "pcars2.h" +//#include "scs2.h" + +#include "simdata.h" +#include "simapi.h" + + + +//typedef struct +//{ +// void* addr; +// int fd; +// union +// { +// ACMap* ac; +// RF2Map* rf2; +// PCars2Map* pcars2; +// SCS2Map* scs2; +// } d; +//} +//SimMap; + +//struct _simmap; +typedef struct _simmap SimMap; + +typedef struct +{ + void* pcars2_addr; + int pcars2_fd; + void* acphysics_addr; + int acphysics_fd; + void* acgraphics_addr; + int acgraphics_fd; + void* acstatic_addr; + int acstatic_fd; + void* acevophysics_addr; + int acevophysics_fd; + void* acevographics_addr; + int acevographics_fd; + void* acevostatic_addr; + int acevostatic_fd; + void* accrew_addr; + int accrew_fd; + void* r3e_addr; + int r3e_fd; + void* lmu_addr; + int lmu_fd; +} +SimCompatMap; + +bool simapi_does_sim_need_bridge(SimulatorEXE s); +SimulatorEXE simapi_get_sim_exe(SimInfo* si); +SimInfo simapi_get_sim(SimData* simdata, SimMap* simmap, bool force_udp, int (*setup_udp)(int), bool simd); + +int simapi_strtogame(const char* game); +char* simapi_gametostr(SimulatorEXE sim); +char* simapi_gametofullstr(SimulatorEXE sim); + +SimMap* simapi_simmap_create(void); + +int simapi_init(SimData* simdata, SimMap* simmap, SimulatorAPI simulator, SimulatorEXE simexe); +int simapi_initudp(SimData* simdata, SimMap* simmap, SimulatorAPI simulator); +int simapi_datamap(SimData* simdata, SimMap* simmap, SimulatorAPI simulator, bool udp, char* base); +int simapi_sim_clear(SimData* simdata, SimMap* simmap, bool issimd); +int simapi_universalmap_open(SimMap* simmap, SimData* simdata); +int simapi_universalmap_free(SimMap* simmap); +int simapi_compatmap_open(SimCompatMap* compatmap); +int simapi_compatmap_free(SimCompatMap* compatmap); +int simapi_compatmap_clear(SimCompatMap* compatmap); + +void simapi_set_proximity_data(SimData* simdata, int cars, int8_t lr_flip); + +void map_suspension_velocity(SimData* simdata, double new_suspension[4]); + +void map_assetto_corsa_data(SimData* simdata, SimMap* simmap, SimulatorEXE simexe); +void map_rfactor2_data(SimData* simdata, SimMap* simmap); +void map_project_cars2_data(SimData* simdata, SimMap* simmap, bool udp, char* base); +void map_trucks_data(SimData* simdata, SimMap* simmap); +void map_outgauge_outsim_data(SimData* simdata, SimMap* simmap, SimulatorEXE simexe, char* base); +void map_dirt_rally_2_data(SimData* simdata, SimMap* simmap, char* base); +void map_f1_2018_data(SimData* simdata, SimMap* simmap, char* base); +void map_wreckfest2_data(SimData* simdata, SimMap* simmap, char* base); +void map_richard_burns_rally_data(SimData* simdata, SimMap* simmap, char* base); +void map_forza_data(SimData* simdata, SimMap* simmap, char* base); +void map_r3e_data(SimData* simdata, SimMap* simmap); + + +#endif diff --git a/stage/usr/lib/libsimapi.so b/stage/usr/lib/libsimapi.so new file mode 120000 index 0000000..a20b362 --- /dev/null +++ b/stage/usr/lib/libsimapi.so @@ -0,0 +1 @@ +libsimapi.so.1 \ No newline at end of file diff --git a/stage/usr/lib/libsimapi.so.1 b/stage/usr/lib/libsimapi.so.1 new file mode 120000 index 0000000..332f99d --- /dev/null +++ b/stage/usr/lib/libsimapi.so.1 @@ -0,0 +1 @@ +libsimapi.so.1.0.1 \ No newline at end of file diff --git a/stage/usr/lib/libsimapi.so.1.0.1 b/stage/usr/lib/libsimapi.so.1.0.1 new file mode 100755 index 0000000..e4641ef Binary files /dev/null and b/stage/usr/lib/libsimapi.so.1.0.1 differ diff --git a/stage/usr/lib/systemd/user/simd.service b/stage/usr/lib/systemd/user/simd.service new file mode 100644 index 0000000..0c65e0c --- /dev/null +++ b/stage/usr/lib/systemd/user/simd.service @@ -0,0 +1,18 @@ +[Unit] +Description=SimAPI Daemon - Racing Simulator Telemetry Service +Documentation=https://github.com/Spacefreak18/simapi +After=default.target + +[Service] +Type=simple +Environment="LD_LIBRARY_PATH=%h/.local/lib" +ExecStart=%h/.local/bin/simd +Restart=on-failure +RestartSec=5 + +# Logging goes to journald +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=default.target diff --git a/stage/usr/share/pkgconfig/simapi.pc b/stage/usr/share/pkgconfig/simapi.pc new file mode 100644 index 0000000..56fa12a --- /dev/null +++ b/stage/usr/share/pkgconfig/simapi.pc @@ -0,0 +1,13 @@ +prefix=/usr +exec_prefix=${prefix} + +libdir=/usr/lib +includedir=/usr/include + +Name: simapi +Description: Telemetry Mapping Library for Racing Sims +Version: 1.0.1 + +Requires: +Libs: -L${libdir} -lmylib +Cflags: -I${includedir} diff --git a/stage/usr/share/simd/simd.config b/stage/usr/share/simd/simd.config new file mode 100644 index 0000000..d61b8af --- /dev/null +++ b/stage/usr/share/simd/simd.config @@ -0,0 +1,61 @@ +sims = +( + // currently this is only for shm compatability and only sims listed here will be considered + { + name = "AssettoCorsa"; + gameid = 244210; + launchexe = "AssettoCorsa.exe"; + liveexe = "acs.exe"; + bridgedelay = 5; // this is the default, this is optional + simapi = 1; // SimulatorAPI enum defined in simapi.h + }, + { + name = "AssettoCorsaCompetizione"; + gameid = 805550; + launchexe = "AC2-Win64-Shipping.exe"; + liveexe = "AC2-Win64-Shipping.exe"; + bridgedelay = 5; // this is the default, this is optional + simapi = 1; // SimulatorAPI enum defined in simapi.h + }, + { + name = "AssettoCorsaEVO"; + gameid = 3058630; + launchexe = "AssettoCorsaEVO.exe"; + liveexe = "AssettoCorsaEVO.exe"; + bridgedelay = 5; // this is the default, this is optional + simapi = 1; // SimulatorAPI enum defined in simapi.h + }, + { + name = "AssettoCorsaRally"; + gameid = 3917090; + launchexe = "acr.exe"; + liveexe = "acr.exe"; + bridgedelay = 5; // this is the default, this is optional + simapi = 1; // SimulatorAPI enum defined in simapi.h + }, + { + name = "Automobilista2"; + gameid = 1066890; + useudp = false; + launchexe = "AMS2AVX.exe"; + liveexe = "AMS2AVX.exe"; + bridgedelay = 5; // this is the default, this is optional + simapi = 3; // SimulatorAPI enum defined in simapi.h + }, + { + name = "DirtRally2"; + gameid = 690790; + launchexe = "dirtrally2.exe"; + liveexe = "dirtrally2.exe"; + bridgedelay = 5; + simapi = 6; // SimulatorAPI enum defined in simapi.h + }, + { + name = "RaceRoomExperience"; + gameid = 211500; + launchexe = "RRRE64.exe"; + liveexe = "RRRE64.exe"; + bridgedelay = 5; + simapi = 8; // SimulatorAPI enum defined in simapi.h + }, +); diff --git a/tools/distro/fedora/rpm/simapi.spec b/tools/distro/fedora/rpm/simapi.spec index b48e972..3fdaeed 100644 --- a/tools/distro/fedora/rpm/simapi.spec +++ b/tools/distro/fedora/rpm/simapi.spec @@ -14,13 +14,17 @@ Requires: procps-ng %description Racing Simulator Telemetry Libraries +# Builds whatever tree has been staged at $RPM_SOURCE_DIR/simapi, cloning +# upstream only if nothing is staged. The unconditional clone this replaced +# meant an rpm's contents tracked the default branch rather than the tag being +# built, so a fix on the branch being released was absent from its own release. +# CI stages the checked-out tree; a bare `rpmbuild -ba` still works as before. %prep rm -rf $RPM_BUILD_DIR/simapi -rm -rf $RPM_SOURCE_DIR/simapi -cd $RPM_SOURCE_DIR -git clone https://github.com/spacefreak18/simapi -cd simapi -cd .. +if [ ! -d $RPM_SOURCE_DIR/simapi ]; then + cd $RPM_SOURCE_DIR + git clone https://github.com/spacefreak18/simapi +fi cp -r $RPM_SOURCE_DIR/simapi $RPM_BUILD_DIR/ %build diff --git a/tools/distro/fedora/rpm/simd.spec b/tools/distro/fedora/rpm/simd.spec index a52289a..62c9a8a 100644 --- a/tools/distro/fedora/rpm/simd.spec +++ b/tools/distro/fedora/rpm/simd.spec @@ -16,24 +16,52 @@ Requires: argtable libconfig libuv libsimapi libyder %description Racing Simulator Telemetry Libraries +# Builds whatever tree has been staged at $RPM_SOURCE_DIR/simapi, cloning +# upstream only if nothing is staged. The unconditional clone this replaced +# meant an rpm's contents tracked the default branch rather than the tag being +# built, so a fix on the branch being released was absent from its own release. +# CI stages the checked-out tree; a bare `rpmbuild -ba` still works as before. %prep rm -rf $RPM_BUILD_DIR/simapi -rm -rf $RPM_SOURCE_DIR/simapi -cd $RPM_SOURCE_DIR -git clone https://github.com/spacefreak18/simapi -cd simapi -cd .. +if [ ! -d $RPM_SOURCE_DIR/simapi ]; then + cd $RPM_SOURCE_DIR + git clone https://github.com/spacefreak18/simapi +fi cp -r $RPM_SOURCE_DIR/simapi $RPM_BUILD_DIR/ %build cd $RPM_BUILD_DIR/simapi -cmake -B build -DBUILD_SIMD=on +# The two *_DIR variables default under $ENV{HOME}; a package must never +# write into a user's home. Redirected rather than patched upstream, since +# both are already CACHE PATH variables. +# CMAKE_SKIP_RPATH: simd links libsimapi out of the build tree, so CMake bakes +# that path in as a RUNPATH and rpm's check-rpaths rejects the resulting +# binary outright ("contains an invalid runpath ... /rpmbuild/BUILD/..."). The +# spec's %%global __brp_check_rpaths %%{nil} no longer suppresses that check on +# Fedora 43/44. Not emitting the RUNPATH is the deterministic fix, and nothing +# needs it: libsimapi installs to a standard system library directory. +cmake -B build -DBUILD_SIMD=on -DCMAKE_INSTALL_PREFIX=/usr \ + -DCMAKE_SKIP_RPATH=ON \ + -DSYSTEMD_USER_UNIT_DIR=/usr/lib/systemd/user \ + -DSIMD_CONFIG_DIR=/usr/share/simd cd build make %install mkdir -p $RPM_BUILD_ROOT/usr/bin cp $RPM_BUILD_DIR/simapi/build/simd/simd $RPM_BUILD_ROOT/usr/bin/simd +# Upstream's simd/conf/simd.service sets Type=simple against a daemon that +# double-forks -- systemd takes the parent's exit as the service failing while +# the real daemon carries on unsupervised -- and hardcodes +# ExecStart=%%h/.local/bin/simd. The packaged unit runs /usr/bin/simd -n. +install -Dm644 $RPM_BUILD_DIR/simapi/tools/distro/simd.service \ + $RPM_BUILD_ROOT/usr/lib/systemd/user/simd.service +# An example, not a live config: packages must not write into $HOME, which is +# where simd looks for it (built from getpwuid(), not $HOME or XDG_CONFIG_HOME). +install -Dm644 $RPM_BUILD_DIR/simapi/simd/conf/simd.config \ + $RPM_BUILD_ROOT/usr/share/simd/simd.config %files /usr/bin/simd +/usr/lib/systemd/user/simd.service +/usr/share/simd/simd.config diff --git a/tools/distro/flatpak/io.github.spacefreak18.simd.desktop b/tools/distro/flatpak/io.github.spacefreak18.simd.desktop new file mode 100644 index 0000000..d60d85b --- /dev/null +++ b/tools/distro/flatpak/io.github.spacefreak18.simd.desktop @@ -0,0 +1,16 @@ +[Desktop Entry] +Name=SimAPI Daemon +Comment=Maps racing simulator telemetry to shared memory for other applications +Exec=simd -n +Icon=io.github.spacefreak18.simd +Terminal=false +Type=Application +Categories=Game;Utility; +# Deliberately listed rather than NoDisplay=true. simd has no window, but the +# entry is how a desktop's "startup applications" picker finds it, which is +# how this is meant to be started -- and appstreamcli compose discards a +# NoDisplay component, leaving the Flatpak with no metadata at all. +# -n (--nodaemon) above is deliberate. simd double-forks by default, and under +# `flatpak run` the sandbox is torn down when the process it started exits -- +# taking the daemon with it. Running in the foreground keeps flatpak's own +# process supervision pointed at the real daemon. diff --git a/tools/distro/flatpak/io.github.spacefreak18.simd.metainfo.xml b/tools/distro/flatpak/io.github.spacefreak18.simd.metainfo.xml new file mode 100644 index 0000000..fea625e --- /dev/null +++ b/tools/distro/flatpak/io.github.spacefreak18.simd.metainfo.xml @@ -0,0 +1,27 @@ + + + io.github.spacefreak18.simd + SimAPI Daemon + Racing simulator telemetry mapped to shared memory + MIT + LGPL-3.0-or-later + +

+ SimAPI Daemon watches for a running racing simulator, maps its telemetry, + and republishes it as a universal shared memory map at + /dev/shm/SIMAPI.DAT for other applications to read. +

+

+ This build can run commands on the host system. It has to: identifying a + running simulator means reading the host's process table, which a Flatpak + sandbox cannot otherwise see. Treat this package as unconfined software + delivered through Flatpak, not as a sandboxed application. +

+
+ + Paul Dino Jones + + io.github.spacefreak18.simd.desktop + https://github.com/Spacefreak18/simapi + +
diff --git a/tools/distro/flatpak/io.github.spacefreak18.simd.yml b/tools/distro/flatpak/io.github.spacefreak18.simd.yml new file mode 100644 index 0000000..993f16f --- /dev/null +++ b/tools/distro/flatpak/io.github.spacefreak18.simd.yml @@ -0,0 +1,251 @@ +# Flatpak manifest for simd, the SimAPI telemetry daemon. +# +# simd is a background service, not an application: it watches for a running +# racing simulator, maps its telemetry, and republishes it at +# /dev/shm/SIMAPI.DAT for other applications to read. It is packaged as a +# Flatpak so it can be installed on immutable distributions, where a .deb or +# .rpm cannot be, and is started the way other Flatpak services are -- an +# entry in ~/.config/autostart running `flatpak run`. +# +# READ THIS BEFORE INSTALLING: this app can run arbitrary commands on the +# host, and its sandbox is therefore not a security boundary. +# +# That is not incidental, it is what the daemon does. simd identifies a +# running sim by scanning /proc for known process names and reads +# /proc//environ for the Steam compat variables behind its auto-bridge. +# Flatpak gives every sandbox its own PID namespace and offers no way to share +# the host's -- measured 634 processes on the host against 4 inside a sandbox, +# unchanged by --allow=devel or --filesystem=host, and /proc cannot be bound +# in ("Path /proc is reserved by Flatpak"). The only mechanism that reaches +# host processes is flatpak-spawn --host, which is general-purpose by nature: +# an app that can run one host command can run any of them. +# +# So --talk-name=org.freedesktop.Flatpak below is doing real work and is also +# the reason this bundle isolates nothing. Flatpak here is a delivery +# mechanism for immutable systems, not a confinement story. Anyone reviewing +# this for a public repository should weigh it on those terms. +app-id: io.github.spacefreak18.simd +runtime: org.freedesktop.Platform +runtime-version: '25.08' +sdk: org.freedesktop.Sdk +command: simd +finish-args: + # How simd reaches the host process table, the environ reads behind the + # auto-bridge, notify-send, and the Proton bridge exec. See the header. + - --talk-name=org.freedesktop.Flatpak + # Where simd publishes telemetry, as POSIX shared memory + # (/dev/shm/SIMAPI.DAT). shm is its own device option and is NOT implied by + # --device=all -- measured: `ls /dev/shm` inside a sandbox without it + # returned 0 entries against 18 on the host. Without this the map simd + # writes is invisible to every reader outside the sandbox, which is the + # entire point of running it. + - --device=shm + # simd builds its config path from getpwuid() rather than $HOME or + # XDG_CONFIG_HOME (simd.c), so it wants the host's real directory. Without + # it simd runs on but logs "Disabling Automatic Bridge Mode" and never + # launches the Windows bridge under Proton. + - --filesystem=xdg-config/simd:create + # Sim telemetry under Proton lives in the game's wine prefix. + - --filesystem=home + - --share=network + +modules: + - name: libconfig + buildsystem: cmake-ninja + config-opts: + # /app/lib for every module, not the lib64 CMake picks by default: + # only /app/lib is on the runtime's library search path, so anything + # landing in lib64 builds and bundles fine and then fails at startup + # with "cannot open shared object file". + - -DCMAKE_INSTALL_LIBDIR=lib + - -DBUILD_TESTS=OFF + # libconfig 1.7.3's CMakeLists.txt predates CMake 3.5's minimum + # version policy; confirmed via a failed local build. + - -DCMAKE_POLICY_VERSION_MINIMUM=3.5 + post-install: + - sh -c 'f=$(ls -1 COPYING COPYING.txt COPYING.LIB LICENSE LICENSE.txt LICENSE.md LICENCE licence.txt 2>/dev/null | head -1); if [ -n "$f" ]; then install -Dm644 "$f" /app/share/licenses/libconfig/$(basename "$f"); else echo "WARNING: no licence file found for libconfig"; fi' + sources: + - type: archive + url: https://github.com/hyperrealm/libconfig/archive/refs/tags/v1.7.3.tar.gz + sha256: 68757e37c567fd026330c8a8449aa5f9cac08a642f213f2687186b903bd7e94e + + - name: argtable2 + buildsystem: cmake-ninja + config-opts: + # /app/lib for every module, not the lib64 CMake picks by default: + # only /app/lib is on the runtime's library search path, so anything + # landing in lib64 builds and bundles fine and then fails at startup + # with "cannot open shared object file". + - -DCMAKE_INSTALL_LIBDIR=lib + # Same CMake-minimum-version issue as libconfig -- this one's + # CMakeLists.txt is even older (2011). + - -DCMAKE_POLICY_VERSION_MINIMUM=3.5 + # 2011-era code missing / includes for + # isspace/toupper/bzero; GCC 14+ made implicit-function-declaration a + # hard error by default where it used to just warn. Confirmed via a + # failed local build. + - -DCMAKE_C_FLAGS=-Wno-error=implicit-function-declaration + # add_library(argtable2 ...) has no explicit STATIC/SHARED, so it + # defaults to CMake's global BUILD_SHARED_LIBS (off by default -> a + # static .a). monocoque's own target_link_libraries() lists argtable2 + # before helper (which needs its symbols, e.g. arg_end/arg_parse in + # parameters.c) -- fine for a shared library (all symbols always + # available regardless of link-line position, which is what real + # distro packages of this ship), but a static archive only pulls + # symbols already needed by things linked *before* it, so helper's + # later reference to argtable2's symbols never resolves. Confirmed + # via a failed local build ("undefined reference to arg_end" etc. + # from helper's parameters.c, even though libargtable2.a genuinely + # contained those symbols). Building shared instead matches how this + # dependency normally ships and avoids the link-order sensitivity + # entirely, rather than reordering every target_link_libraries() call + # in CMakeLists.txt. + - -DBUILD_SHARED_LIBS=ON + # This CMake port has no install() rules at all (confirmed: no + # CMakeLists.txt in the tree calls install()), so the default + # cmake-ninja `ninja install` step silently does nothing -- monocoque's + # own build then fails on a missing argtable2.h. build-commands here + # replace the default install step: `ninja` still does the real + # compile, then copy the header and whatever library form actually got + # built (glob rather than a fixed versioned filename, since the exact + # .so version suffix isn't predictable without a build). + build-commands: + - ninja + - install -Dm644 src/argtable2.h /app/include/argtable2.h + - | + for f in src/libargtable2.so* src/libargtable2.a; do + if [ -e "$f" ]; then cp -P "$f" /app/lib/; fi + done + true + post-install: + - sh -c 'f=$(ls -1 COPYING COPYING.txt COPYING.LIB LICENSE LICENSE.txt LICENSE.md LICENCE licence.txt 2>/dev/null | head -1); if [ -n "$f" ]; then install -Dm644 "$f" /app/share/licenses/argtable2/$(basename "$f"); else echo "WARNING: no licence file found for argtable2"; fi' + sources: + - type: archive + url: https://sourceforge.net/projects/argtable/files/argtable/argtable-2.13/argtable2-13.tar.gz/download + sha256: 8f77e8a7ced5301af6e22f47302fdbc3b1ff41f2b83c43c77ae5ca041771ddbf + dest-filename: argtable2-13.tar.gz + + - name: libuv + buildsystem: cmake-ninja + config-opts: + # /app/lib for every module, not the lib64 CMake picks by default: + # only /app/lib is on the runtime's library search path, so anything + # landing in lib64 builds and bundles fine and then fails at startup + # with "cannot open shared object file". + - -DCMAKE_INSTALL_LIBDIR=lib + - -DBUILD_TESTING=OFF + post-install: + - sh -c 'f=$(ls -1 COPYING COPYING.txt COPYING.LIB LICENSE LICENSE.txt LICENSE.md LICENCE licence.txt 2>/dev/null | head -1); if [ -n "$f" ]; then install -Dm644 "$f" /app/share/licenses/libuv/$(basename "$f"); else echo "WARNING: no licence file found for libuv"; fi' + sources: + - type: archive + url: https://github.com/libuv/libuv/archive/refs/tags/v1.49.2.tar.gz + sha256: 388ffcf3370d4cf7c4b3a3205504eea06c4be5f9e80d2ab32d19f8235accc1cf + + # orcania and yder are what simd logs through. Neither is in the freedesktop + # runtime and neither is packaged for Fedora either (the rpm legs vendor both + # as static libraries); here they are ordinary /app/lib shared libraries that + # ship inside the bundle. + # + # CMAKE_C_FLAGS_RELEASE carries -Wno-error because both projects append their + # own `-Wall -Werror` to CMAKE_C_FLAGS -- setting that variable would be + # overridden, while the _RELEASE flags land after it where the last of + # -Werror/-Wno-error wins. orcania 2.3.3 assigns strstr() on a const pointer + # to a char*, which newer compilers reject outright. + + # orcania and yder are what simd logs through. Neither is in the freedesktop + # runtime and neither is packaged for Fedora either (the rpm legs vendor both + # as static libraries); here they are ordinary /app/lib shared libraries that + # ship inside the bundle. + # + # CMAKE_C_FLAGS_RELEASE carries -Wno-error because both projects append their + # own `-Wall -Werror` to CMAKE_C_FLAGS -- setting that variable would be + # overridden, while the _RELEASE flags land after it where the last of + # -Werror/-Wno-error wins. orcania 2.3.3 assigns strstr() on a const pointer + # to a char*, which newer compilers reject outright. + - name: orcania + buildsystem: cmake-ninja + config-opts: + # Everything into /app/lib, not the lib64 CMake picks by default here: + # orcania and yder both landed in /app/lib64, so when yder configured, + # pkg-config did not find liborcania.pc under /app/lib/pkgconfig and + # yder linked without it. The failure surfaced much later and looked + # unrelated -- simd failed to link with undefined references to + # o_malloc/o_free/o_strdup/split_string coming out of libyder.so. + - -DCMAKE_INSTALL_LIBDIR=lib + - -DCMAKE_BUILD_TYPE=Release + - -DCMAKE_C_FLAGS_RELEASE=-O2 -DNDEBUG -Wno-error + - -DBUILD_ORCANIA_TESTING=OFF + post-install: + - sh -c 'f=$(ls -1 COPYING COPYING.txt COPYING.LIB LICENSE LICENSE.txt LICENSE.md LICENCE licence.txt 2>/dev/null | head -1); if [ -n "$f" ]; then install -Dm644 "$f" /app/share/licenses/orcania/$(basename "$f"); else echo "WARNING: no licence file found for orcania"; fi' + sources: + - type: git + url: https://github.com/babelouest/orcania + commit: ffc8b55d09a3488f4f6be38034b33bc64bf8b0ce + + - name: yder + buildsystem: cmake-ninja + config-opts: + # Everything into /app/lib, not the lib64 CMake picks by default here: + # orcania and yder both landed in /app/lib64, so when yder configured, + # pkg-config did not find liborcania.pc under /app/lib/pkgconfig and + # yder linked without it. The failure surfaced much later and looked + # unrelated -- simd failed to link with undefined references to + # o_malloc/o_free/o_strdup/split_string coming out of libyder.so. + - -DCMAKE_INSTALL_LIBDIR=lib + - -DCMAKE_BUILD_TYPE=Release + - -DCMAKE_C_FLAGS_RELEASE=-O2 -DNDEBUG -Wno-error + # Journald would need systemd-devel for a backend simd never selects; + # its config offers file and syslog. + - -DWITH_JOURNALD=OFF + - -DBUILD_YDER_TESTING=OFF + post-install: + - sh -c 'f=$(ls -1 COPYING COPYING.txt COPYING.LIB LICENSE LICENSE.txt LICENSE.md LICENCE licence.txt 2>/dev/null | head -1); if [ -n "$f" ]; then install -Dm644 "$f" /app/share/licenses/yder/$(basename "$f"); else echo "WARNING: no licence file found for yder"; fi' + sources: + - type: git + url: https://github.com/babelouest/yder + commit: dffe82c0483bb95d0d518ba1e36c568e63a24628 + + # simapi built through its *own* CMakeLists, unlike the monocoque module + # below which compiles the same sources into a static library of its own. + # That is what produces libsimapi.so and, with BUILD_SIMD, the simd binary -- + # pinned to the exact commit monocoque's submodule tracks, so the daemon and + # the app in this bundle agree on the SimData layout they map. + + - name: simd + buildsystem: cmake-ninja + config-opts: + - -DCMAKE_BUILD_TYPE=Release + - -DBUILD_SIMD=ON + # Both default ON and install into $ENV{HOME} -- a systemd user unit and + # a config file -- which during a flatpak-builder run would land in the + # build user's home rather than the prefix. The unit is meaningless in a + # Flatpak anyway; autostart is a ~/.config/autostart entry instead. + - -DINSTALL_SYSTEMD_SERVICE=off + - -DSIMD_CONFIG_DIR=/app/share/simd + # Pinned rather than left to GNUInstallDirs, which picks lib64 here and + # the multiarch triplet on Debian. Every consumer of this prefix then + # has to guess which, and both guesses have already been wrong once. + - -DCMAKE_INSTALL_LIBDIR=lib + build-options: + env: + LIBRARY_PATH: /app/lib + post-install: + - install -Dm644 LICENSE.rst /app/share/licenses/simd/LICENSE.rst + - install -Dm644 tools/distro/flatpak/io.github.spacefreak18.simd.desktop /app/share/applications/io.github.spacefreak18.simd.desktop + - install -Dm644 tools/distro/flatpak/io.github.spacefreak18.simd.metainfo.xml /app/share/metainfo/io.github.spacefreak18.simd.metainfo.xml + # PNG only, deliberately. Installing the SVG as well fails the build: + # appstreamcli in the freedesktop SDK cannot decode it at all and + # aborts compose with "Unrecognized image file format", which surfaces + # only as "appstreamcli compose failed". Reproduced and confirmed fixed + # by running compose against a staged prefix. + - install -Dm644 tools/distro/simd-128.png /app/share/icons/hicolor/128x128/apps/io.github.spacefreak18.simd.png + sources: + - type: dir + path: ../../.. + # Local build trees are not part of the source. Without these a + # developer's build/ and stage/ get copied into the sandbox, where a + # stale CMakeCache.txt pointing at host paths breaks the configure + # step in ways that look nothing like their cause. + skip: + - build + - stage diff --git a/tools/distro/simd-128.png b/tools/distro/simd-128.png new file mode 100644 index 0000000..0b5a5a9 Binary files /dev/null and b/tools/distro/simd-128.png differ diff --git a/tools/distro/simd.service b/tools/distro/simd.service new file mode 100644 index 0000000..5044aec --- /dev/null +++ b/tools/distro/simd.service @@ -0,0 +1,28 @@ +[Unit] +Description=SimAPI Daemon - Racing Simulator Telemetry Service +Documentation=https://github.com/Spacefreak18/simapi +After=default.target + +[Service] +# -n (--nodaemon), not the default: simd double-forks on its own, and under +# Type=simple systemd would take the parent's exit as the service dying and +# report it failed while the real daemon carried on unsupervised. Letting +# systemd own the process instead of the double fork also makes Restart= +# and journald logging work as written. +# +# Type=forking with PIDFile=/tmp/simd.pid would be the alternative, but simd +# writes that path unconditionally and a world-writable pidfile in /tmp is +# not something to build a service around. +ExecStart=/usr/bin/simd -n +Restart=on-failure +RestartSec=5 + +# No LD_LIBRARY_PATH: libsimapi is installed to the system library path by +# the package, unlike upstream's conf/simd.service which assumes a build +# installed into ~/.local. + +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=default.target diff --git a/tools/distro/simd.svg b/tools/distro/simd.svg new file mode 100644 index 0000000..1cb4c3b --- /dev/null +++ b/tools/distro/simd.svg @@ -0,0 +1,14 @@ + + + + + + + + + + +