Mercurial > libanimone
changeset 0:a76fa32bdc92
*: HUUUGE changes
animia has been renamed to animone, so instead of thinking of a
health condition, you think of a beautiful flower :)
I've also edited some of the code for animone, but I have no idea
if it even works or not because I don't have a mac or windows
machine lying around. whoops!
... anyway, all of the changes divergent from Anisthesia are now
licensed under BSD. it's possible that I could even rewrite most
of the code to where I don't even have to keep the MIT license,
but that's thinking too far into the future
I've been slacking off on implementing the anime seasons page,
mostly out of laziness. I think I'd have to create another db file
specifically for the seasons
anyway, this code is being pushed *primarily* because the hard drive
it's on is failing! yay :)
author | Paper <paper@paper.us.eu.org> |
---|---|
date | Mon, 01 Apr 2024 02:43:44 -0400 |
parents | |
children | 0c3cc9e6cd84 |
files | .clang-format .hgignore LICENSE.BSD LICENSE.MIT Makefile.am README.md configure.ac data/players.anisthesia include/animone.h include/animone/fd.h include/animone/fd/kvm.h include/animone/fd/proc.h include/animone/fd/win32.h include/animone/fd/xnu.h include/animone/media.h include/animone/player.h include/animone/strategies.h include/animone/types.h include/animone/util.h include/animone/util/osx.h include/animone/util/win32.h include/animone/win.h include/animone/win/quartz.h include/animone/win/win32.h include/animone/win/x11.h src/animone.cc src/fd.cc src/fd/kvm.cc src/fd/proc.cc src/fd/win32.cc src/fd/xnu.cc src/player.cc src/strategist.cc src/util.cc src/util/osx.cc src/util/win32.cc src/win.cc src/win/quartz.cc src/win/win32.cc src/win/x11.cc |
diffstat | 40 files changed, 3367 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.clang-format Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,38 @@ +--- +BasedOnStyle: LLVM +UseTab: ForIndentation +PointerAlignment: Left +ColumnLimit: 120 +IndentWidth: 4 +TabWidth: 4 + +# hack!!! +AccessModifierOffset: -4 + +IndentCaseLabels: true +IndentAccessModifiers: false +IndentPPDirectives: AfterHash + +BreakBeforeBraces: Attach +BreakStringLiterals: true + +AlwaysBreakTemplateDeclarations: true + +SpaceAfterTemplateKeyword: false + +AlignAfterOpenBracket: Align +AlignArrayOfStructures: Left +AlignEscapedNewlines: DontAlign +AlignConsecutiveMacros: true + +AllowShortIfStatementsOnASingleLine: false +AllowShortBlocksOnASingleLine: Empty +AllowShortEnumsOnASingleLine: false +AllowShortFunctionsOnASingleLine: InlineOnly +AllowShortCaseLabelsOnASingleLine: true + +Cpp11BracedListStyle: true + +--- +Language: Cpp +Standard: c++17
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgignore Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,40 @@ +syntax: glob + +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +syntax: regexp + +# Build dir +^build/ +^test/build/ \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/LICENSE.BSD Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2023-2024, Paper +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/LICENSE.MIT Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2017 Eren Okka +Copyright (c) 2023 Paper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Makefile.am Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,83 @@ +lib_LTLIBRARIES = libanimone.la + +include_HEADERS = \ + include/animone.h + +animiadir = $(includedir)/animone +nobase_animia_HEADERS = \ + include/animone/media.h \ + include/animone/player.h \ + include/animone/types.h + +noinst_HEADERS = \ + include/animone/fd/kvm.h \ + include/animone/fd/proc.h \ + include/animone/fd/win32.h \ + include/animone/fd/xnu.h \ + include/animone/util/osx.h \ + include/animone/util/win32.h \ + include/animone/win/quartz.h \ + include/animone/win/win32.h \ + include/animone/win/x11.h \ + include/animone/fd.h \ + include/animone/strategies.h \ + include/animone/util.h \ + include/animone/win.h + +if BUILD_WIN +files_win = src/fd/win32.cc src/win/win32.cc src/util/win32.cc +libs_win = -lole32 -luuid +endif + +if BUILD_OSX +files_osx = src/fd/xnu.cc src/win/quartz.cc src/util/osx.cc +libs_osx = -lobjc +ldflags_osx = -framework Foundation -framework CoreGraphics -framework ApplicationServices +endif + +if BUILD_LINUX +files_linux = src/fd/proc.cc +endif + +# these should be in standard locations anyway +if BUILD_LIBUTIL +libs_libutil = -lutil +endif + +if BUILD_LIBKVM +files_libkvm = src/fd/kvm.cc +libs_libkvm = -lkvm +endif + +if BUILD_XCB +files_x11 = src/win/x11.cc +cflags_x11 = $(XCB_CFLAGS) +libs_x11 = $(XCB_LIBS) +endif + +EXTRA_DIST = \ + $(top_srcdir)/data/players.anisthesia + +libanimone_la_SOURCES = \ + src/animone.cc \ + src/fd.cc \ + src/player.cc \ + src/strategist.cc \ + src/util.cc \ + src/win.cc \ + $(files_win) \ + $(files_osx) \ + $(files_linux) \ + $(files_libutil) \ + $(files_libkvm) \ + $(files_x11) \ + $(files_wayland) + +libanimone_la_CPPFLAGS = -I$(top_srcdir)/include $(DEFS) + +libanimone_la_CXXFLAGS = -std=c++17 $(cflags_osx) $(cflags_x11) $(cflags_wayland) +libanimone_la_LDFLAGS = -version-info 0:0:0 $(ldflags_osx) + +libanimone_la_LIBADD = $(libs_win) $(libs_wayland) $(libs_x11) $(libs_osx) $(libs_libutil) $(libs_libkvm) + +ACLOCAL_AMFLAGS = -I m4
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/README.md Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,36 @@ +# Animia +Animia is a work-in-progress cross-platform hard fork of Anisthesia and part of +Minori. + +Most (if not all) Anisthesia configs should also work in this library as well +(at least on Windows). + +## License +Changes divergent from Anisthesia are under the BSD 3-clause license. You can +find a copy of the original MIT license bundled with Anisthesia at `LICENSE.MIT` +in the root folder. + +## Support +Unlike Anisthesia, Animia currently does not support UI automation, i.e., most +web browsers will not work properly, if at all. + +Animia will first attempt to connect to a windowing system. If that fails, it falls +back to just enumerating over the open processes in the system. + +## Platform-specific quirks + +### Windows +To get the currently opened file handles on Windows, Animia has to use internal +kernel functions. However, these functions aren't likely to change anytime soon. + +### macOS +The code to retrieve executable names on macOS uses internal functions. However, +if these functions cannot be found for whatever reason, it falls back to parsing +the arguments, and then to calling the kernel. + +Additionally, macOS does not have the concept of class names, rather, it has +bundle identifiers, which are a suitable replacement in most use cases, and are +what Animia will try to grab before falling back to the Quartz window name. + +### X11 +Animia requires that the XRes extension is installed to retrieve window PIDs.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/configure.ac Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,75 @@ +AC_INIT([animone], [0.1.0-alpha.1]) + +AC_CANONICAL_HOST + +AC_CONFIG_SRCDIR([src/animone.cc]) +AC_CONFIG_AUX_DIR([build-aux]) +AC_CONFIG_MACRO_DIRS([m4]) + +AM_INIT_AUTOMAKE([-Wall -Werror foreign subdir-objects]) + +# Do we have a C++17 compiler +AC_PROG_CXX + +AM_PROG_AR +LT_INIT + +build_win32=no +build_osx=no +build_linux=no +build_libutil=no +build_kvm=no + +build_x11=no + +case "${host_os}" in + cygwin*|mingw*) + # Windows + build_windows=yes + AC_CHECK_TOOL([WINDRES], [windres]) + AC_SUBST(WINDRES) + AC_DEFINE([WIN32]) + ;; + darwin*) + # Mac OS X + build_osx=yes + AC_DEFINE([MACOSX]) + ;; + linux*) + build_linux=yes + AC_DEFINE([LINUX]) + ;; + *) + # FreeBSD + AC_CHECK_LIB([util], [kinfo_getfile], [build_libutil=yes], [build_libutil=no]) + if test "x$build_libutil" = "xyes"; then + AC_DEFINE([LIBUTIL]) + else + # OpenBSD + AC_CHECK_LIB([kvm], [kvm_getfiles], [build_kvm=yes], [build_kvm=no]) + if test "x$build_kvm" = "xyes"; then + AC_DEFINE([LIBKVM]) + fi + fi + ;; +esac + +if test "x$build_osx" = "xno" && test "x$build_windows" = "xno"; then + PKG_CHECK_MODULES(XCB, [xcb xcb-res], [build_x11=yes], [build_x11=no]) + if test "x$build_x11" = "xyes"; then + AC_DEFINE([X11]) + AC_SUBST([XCB_LIBS]) + AC_SUBST([XCB_CFLAGS]) + fi +fi + +AM_CONDITIONAL([BUILD_WIN], [test "x$build_windows" = "xyes"]) +AM_CONDITIONAL([BUILD_OSX], [test "x$build_osx" = "xyes"]) +AM_CONDITIONAL([BUILD_LINUX], [test "x$build_linux" = "xyes"]) +AM_CONDITIONAL([BUILD_LIBUTIL], [test "x$build_libutil" = "xyes"]) +AM_CONDITIONAL([BUILD_LIBKVM], [test "x$build_kvm" = "xyes"]) + +AM_CONDITIONAL([BUILD_XCB], [test "x$build_x11" = "xyes"]) + +AC_CONFIG_FILES([Makefile]) +AC_OUTPUT
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/data/players.anisthesia Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,496 @@ +# This file includes media player data for Anisthesia. It is used to detect +# running players and retrieve information about current media. +# +# Please read before editing this file: +# - Indentation is significant. You must use tabs rather than spaces. +# - Regular expressions begin with a '^' character. ECMAScript grammar is used. +# +# The latest version of this file can be found at: +# <https://github.com/erengy/anisthesia> +# +# This file is in the public domain. + +5KPlayer + windows: + Qt5QWindowIcon + executables: + 5KPlayer + strategies: + open_files + +Ace Player HD + windows: + QWidget + executables: + ace_player + strategies: + # Must be enabled from: Advanced Preferences -> Interface -> Main + # interfaces -> Qt -> Show playing item name in window title + # + # We use the last alternative to avoid detecting other windows such as + # Preferences dialog, which has the same generic class name. + window_title: + ^Ace Player HD.*|(.+) - Ace Player HD.*|.+ + +ALLPlayer + windows: + TApplication + executables: + ALLPlayer + strategies: + open_files + +Baka MPlayer + windows: + Qt5QWindowIcon + executables: + Baka MPlayer + strategies: + open_files + # We cannot avoid detecting other windows such as Preferences dialog, which + # has the same generic class name. + window_title: + ^Baka MPlayer|(.+) + +BESTplayer + windows: + TBESTplayerApp.UnicodeClass + executables: + BESTplayer + strategies: + open_files + window_title: + ^BESTplayer.*|(.+) - BESTplayer.* + +bomi + windows: + Qt5QWindowGLOwnDCIcon + executables: + bomi + strategies: + open_files + window_title: + ^bomi|(.+) - bomi + +BS.Player + windows: + BSPlayer + executables: + bsplayer + strategies: + open_files + +DivX Player + windows: + Qt5QWindowIcon + QWidget + executables: + DivX Player + DivX Plus Player + strategies: + open_files + +GOM Player + windows: + GomPlayer1.x + GomPlayerPlus32_2.x + GomPlayerPlus64_2.x + executables: + GOM + GOM64 + strategies: + open_files + window_title: + ^GOM Player(?: Plus)|(.+)(?:\[Subtitle\]) - GOM Player(?: Plus) + +Kantaris + windows: + ^WindowsForms10\.Window\.20008\.app\..+ + executables: + Kantaris + KantarisMain + strategies: + open_files + window_title: + ^Kantaris.*|(.+) \d{2}:\d{2}:\d{2} - \d{2}:\d{2}:\d{2} + +KMPlayer + windows: + KMPlayer 64X + TApplication + executables: + KMPlayer + KMPlayer64 + strategies: + open_files + window_title: + ^(?:The )?KMPlayer|(?:\[\d+/\d+\] )?(.+) - (?:The )?KMPlayer|(.+) + +Kodi + windows: + Kodi + XBMC + executables: + kodi + XBMC + strategies: + open_files + +Light Alloy + windows: + TApplication + executables: + LA + strategies: + open_files + window_title: + ^Light Alloy.*|(.+) - Light Alloy.* + +Media Player Classic + windows: + MediaPlayerClassicW + executables: + mplayerc + mplayerc64 + strategies: + open_files + # Depends on: Options -> Player -> Title bar + window_title: + ^Media Player Classic|(.+) - Media Player Classic + +Media Player Classic Qute Theater + windows: + ^Qt.+QWindowIcon + executables: + mpc-qt + strategies: + open_files + # Depends on: Options -> Player -> Title bar + # + # We use the last alternative to avoid detecting other windows such as + # Options dialog, which has the same generic class name. + window_title: + ^Media Player Classic Qute Theater|Media Player Classic Qute Theater - (.+)|.+ + +Memento + windows: + ^Qt.+QWindowIcon + executables: + memento + strategies: + open_files + window_title: + ^Memento|(.+) - Memento + +Miro + windows: + gdkWindowToplevel + executables: + Miro + strategies: + open_files + +MPC-BE + windows: + MediaPlayerClassicW + MPC-BE + executables: + mpc-be + mpc-be64 + strategies: + open_files + # Depends on: Options -> Player -> Title bar + window_title: + ^MPC-BE.*|(.+) - MPC-BE.* + +MPC-HC + windows: + MediaPlayerClassicW + executables: + mpc-hc + mpc-hc64 + # Some codec installers append "_nvo" to the filename, if NVIDIA Optimus + # is present on the system. Similarly, various guides recommend + # appending "-gpu", etc. in order to fix some GPU-related issues. + ^mpc-hc.+ + # LAV Filters Megamix + iris + shoukaku + strategies: + open_files + # Depends on: Options -> Player -> Title bar + window_title: + ^Media Player Classic Home Cinema|MPC-HC|(.+) + +MPCSTAR + windows: + ^wxWindow@.* + wxWindowClassNR + executables: + mpcstar + strategies: + open_files + window_title: + ^MPCSTAR.*|(.+) - MPCSTAR.* + +MPDN + windows: + ^WindowsForms10\.Window\.8\.app\..+ + executables: + MediaPlayerDotNet + strategies: + open_files + window_title: + ^MPDN - Media Player .NET \((?:32|64)-bit Edition\)|(.*) - MPDN \((?:32|64)-bit Edition\) + +mpv + windows: + mpv + executables: + mpv + strategies: + open_files + # May be in an unexpected format if "--title" option is used. Ideally, it + # should return only "${filename}", "${path}" or "${media-title}". + window_title: + ^No file - mpv|(.+) - mpv|mpv - (.+) + +mpv.net + windows: + ^WindowsForms10\.Window\.8\.app\..+ + executables: + mpvnet + strategies: + open_files + window_title: + ^mpv\.net.*|(.+) - mpv\.net.* + +MV2Player + windows: + TApplication + executables: + Mv2Player + Mv2PlayerPlus + strategies: + open_files + # Depends on: Options -> Player -> Constant app. title + window_title: + ^MV2 Player|(.+) + +PotPlayer + windows: + PotPlayer + PotPlayer64 + executables: + PotPlayer + PotPlayer64 + PotPlayerMini + PotPlayerMini64 + # LAV Filters Megamix + sumire + zuikaku + strategies: + open_files + window_title: + ^PotPlayer|(.+) - PotPlayer + +SMPlayer + windows: + # Qt5QWindowIcon, Qt5152QWindowIcon, etc. + ^Qt.+QWindowIcon + # Older versions + QWidget + executables: + smplayer + smplayer2 + strategies: + # "open_files" strategy does not work here, because files are loaded by + # a child process of SMPlayer (mplayer or mpv, depending on the selected + # multimedia engine). + # + # We use the last alternative to avoid detecting other windows such as + # Preferences dialog, which has the same generic class name. + window_title: + ^SMPlayer|(.+) - SMPlayer|.+ + +Splash + windows: + DX_DISPLAY0 + executables: + Splash + SplashLite + strategies: + open_files + +SPlayer + windows: + MediaPlayerClassicW + executables: + splayer + strategies: + open_files + # Does not work in theater mode. + window_title: + ^SPlayer|(?:\[(?:GPU Accel\+)?EVR\] )?(.+) - SPlayer + +UMPlayer + windows: + QWidget + executables: + umplayer + strategies: + # "open_files" strategy does not work here, because files are loaded by + # a child process of UMPlayer (mplayer). + # + # We use the last alternative to avoid detecting other windows such as + # Preferences dialog, which has the same generic class name. + window_title: + ^UMPlayer|(.+) - UMPlayer|.+ + +VLC media player + windows: + # Qt5QWindowIcon, Qt5151QWindowIcon, etc. + ^Qt.+QWindowIcon + # Older versions + QWidget + # Skinnable interface + SkinWindowClass + # X11 + vlc + executables: + vlc + strategies: + open_files + # Must be enabled from: Advanced Preferences -> Interface -> Main + # interfaces -> Qt -> Show playing item name in window title + # + # We use the last alternative to avoid detecting other windows such as + # Preferences dialog, which has the same generic class name. + window_title: + ^VLC media player|(.+) - VLC media player|.+ + +WebTorrent Desktop + windows: + Chrome_WidgetWin_1 + executables: + WebTorrent + strategies: + window_title: + ^WebTorrent(?: \(BETA\))?|Main Window|Preferences|About WebTorrent.*|(.+) + +Winamp + windows: + Winamp v1.x + executables: + winamp + strategies: + open_files + window_title: + ^Winamp [\d.]+ Build \d+|\d+\. (.+) - Winamp(?: \[.+\])? + +Windows Media Player + windows: + WMPlayerApp + WMP Skin Host + executables: + wmplayer + strategies: + open_files + +Zoom Player + windows: + TApplication + executables: + zplayer + strategies: + open_files + window_title: + ^Zoom Player|(.+) - Zoom Player (?:FREE|MAX) + +################################################################################ +# Web browsers + +Brave Browser + windows: + Chrome_WidgetWin_1 + executables: + brave + strategies: + ui_automation + window_title: + ^(.+) \(Private\)(?: - Brave)?|(.+) - Brave|(.+) + type: + web_browser + +Google Chrome + windows: + Chrome_WidgetWin_1 + executables: + chrome + strategies: + ui_automation + window_title: + ^(.+) \(Incognito\)(?: - Google Chrome)?|(.+) - Google Chrome|(.+) + type: + web_browser + +Internet Explorer + windows: + IEFrame + executables: + iexplore + strategies: + ui_automation + window_title: + ^(.+) - Internet Explorer(?: - \[InPrivate\])? + type: + web_browser + +Microsoft Edge + windows: + Chrome_WidgetWin_1 + executables: + msedge + strategies: + ui_automation + window_title: + ^(.+) and \d+ more pages? - .+|(.+) - [^-]+ - Microsoft.*Edge|(.+) + type: + web_browser + +Mozilla Firefox + windows: + MozillaUIWindowClass + MozillaWindowClass + executables: + firefox + strategies: + ui_automation + window_title: + ^(?:Mozilla Firefox|Firefox Developer Edition)|(.+) (?:-|—) (?:Mozilla Firefox|Firefox Developer Edition)(?: \(Private Browsing\))? + type: + web_browser + +Opera + windows: + Chrome_WidgetWin_1 + executables: + opera + strategies: + ui_automation + window_title: + ^(.+) - Opera(?: \(Private\))? + type: + web_browser + +Waterfox + windows: + MozillaWindowClass + executables: + waterfox + strategies: + ui_automation + window_title: + ^(.+) - Waterfox(?: \(Private Browsing\))? + type: + web_browser
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/include/animone.h Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,37 @@ +#ifndef ANIMONE_ANIMONE_H_ +#define ANIMONE_ANIMONE_H_ + +#include "animone/media.h" +#include "animone/player.h" +#include "animone/types.h" + +namespace animone { + +enum class ResultType { + Process, + Window +}; + +struct Process { + internal::pid_t pid = 0; /* pid_t == DWORD on Windows, from <sys/types.h> everywhere else */ + std::string name; +}; + +struct Window { + unsigned int id = 0; + std::string class_name; + std::string text; /* title bar text */ +}; + +struct Result { + Player player; + Process process; + Window window; /* has nothing under process mode */ + std::vector<Media> media; +}; + +bool GetResults(const std::vector<Player>& players, std::vector<Result>& results); + +} // namespace animone + +#endif // ANIMONE_ANIMONE_H_
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/include/animone/fd.h Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,32 @@ +#ifndef ANIMONE_ANIMONE_FD_H_ +#define ANIMONE_ANIMONE_FD_H_ + +#include <functional> +#include <set> +#include <string> + +#include "animone/types.h" + +namespace animone { + +struct Process; + +namespace internal { + +struct OpenFile { + pid_t pid = 0; + std::string path; +}; + +using process_proc_t = std::function<bool(const Process&)>; + +using open_file_proc_t = std::function<bool(const OpenFile&)>; + +bool EnumerateOpenProcesses(process_proc_t process_proc); +bool EnumerateOpenFiles(const std::set<pid_t>& pids, open_file_proc_t open_file_proc); + +} // namespace internal + +} // namespace animone + +#endif // ANIMONE_ANIMONE_FD_H_
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/include/animone/fd/kvm.h Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,17 @@ +#ifndef ANIMONE_ANIMONE_FD_KVM_H_ +#define ANIMONE_ANIMONE_FD_KVM_H_ + +#include <set> +#include <string> + +#include "animone/fd.h" +#include "animone/types.h" + +namespace animone::internal::kvm { + +bool EnumerateOpenProcesses(process_proc_t process_proc); +bool EnumerateOpenFiles(const std::set<pid_t>& pids, open_file_proc_t open_file_proc); + +} // namespace animone::internal::kvm + +#endif // ANIMONE_ANIMONE_FD_KVM_H_
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/include/animone/fd/proc.h Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,18 @@ +#ifndef ANIMONE_ANIMONE_FD_PROC_H_ +#define ANIMONE_ANIMONE_FD_PROC_H_ + +#include <set> +#include <string> + +#include "animone/fd.h" +#include "animone/types.h" + +namespace animone::internal::proc { + +bool EnumerateOpenProcesses(process_proc_t process_proc); +bool EnumerateOpenFiles(const std::set<pid_t>& pids, open_file_proc_t open_file_proc); +bool GetProcessName(pid_t pid, std::string& result); + +} // namespace animone::internal::proc + +#endif // ANIMONE_ANIMONE_FD_PROC_H_
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/include/animone/fd/win32.h Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,19 @@ +#ifndef ANIMONE_ANIMONE_FD_WIN32_H_ +#define ANIMONE_ANIMONE_FD_WIN32_H_ + +#include <set> +#include <string> + +#include <windows.h> + +#include "animone/fd.h" +#include "animone/types.h" + +namespace animone::internal::win32 { + +bool EnumerateOpenProcesses(process_proc_t process_proc); +bool EnumerateOpenFiles(const std::set<pid_t>& pids, open_file_proc_t open_file_proc); + +} // namespace animone::internal::win32 + +#endif // ANIMONE_ANIMONE_FD_WIN32_H_
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/include/animone/fd/xnu.h Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,17 @@ +#ifndef ANIMONE_ANIMONE_FD_XNU_H_ +#define ANIMONE_ANIMONE_FD_XNU_H_ + +#include <set> +#include <string> + +#include "animone/fd.h" +#include "animone/types.h" + +namespace animone::internal::xnu { + +bool EnumerateOpenProcesses(process_proc_t process_proc); +bool EnumerateOpenFiles(const std::set<pid_t>& pids, open_file_proc_t open_file_proc); + +} // namespace animone::internal::xnu + +#endif // ANIMONE_ANIMONE_FD_XNU_H_
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/include/animone/media.h Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,32 @@ +#ifndef ANIMONE_ANIMONE_MEDIA_H_ +#define ANIMONE_ANIMONE_MEDIA_H_ + +#include <chrono> +#include <functional> +#include <string> +#include <vector> + +namespace animone { + +using media_time_t = std::chrono::milliseconds; + +enum class MediaInfoType { + Unknown, + File, + Tab, + Title, + Url +}; + +struct MediaInfo { + MediaInfoType type = MediaInfoType::Unknown; + std::string value; +}; + +struct Media { + std::vector<MediaInfo> information; +}; + +} // namespace animone + +#endif // ANIMONE_ANIMONE_MEDIA_H_
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/include/animone/player.h Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,34 @@ +#ifndef ANIMONE_ANIMONE_PLAYER_H_ +#define ANIMONE_ANIMONE_PLAYER_H_ + +#include <string> +#include <vector> + +namespace animone { + +enum class Strategy { + WindowTitle, + OpenFiles, + UiAutomation // unused +}; + +enum class PlayerType { + Default, + WebBrowser // unused +}; + +struct Player { + PlayerType type = PlayerType::Default; + std::string name; + std::string window_title_format; + std::vector<std::string> windows; + std::vector<std::string> executables; + std::vector<Strategy> strategies; +}; + +bool ParsePlayersData(const std::string& data, std::vector<Player>& players); +bool ParsePlayersFile(const std::string& path, std::vector<Player>& players); + +} // namespace animone + +#endif // ANIMONE_ANIMONE_PLAYER_H_ \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/include/animone/strategies.h Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,13 @@ +#ifndef ANIMONE_ANIMONE_STRATEGIES_H_ +#define ANIMONE_ANIMONE_STRATEGIES_H_ + +#include "animone.h" +#include <vector> + +namespace animone::internal { + +bool ApplyStrategies(std::vector<Result>& results); + +} + +#endif // ANIMONE_ANIMONE_STRATEGIES_H_ \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/include/animone/types.h Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,18 @@ +#ifndef ANIMONE_ANIMONE_TYPES_H_ +#define ANIMONE_ANIMONE_TYPES_H_ + +/* define this as unsigned long (DWORD) on win32 so we + don't force the user to include <windows.h> or <IntBase.h> */ +#ifdef _WIN32 +namespace animone::internal { +typedef unsigned long pid_t; +} +#else +/* <sys/types.h> shouldn't be that big, right? */ +# include <sys/types.h> +namespace animone::internal { +typedef ::pid_t pid_t; +} +#endif + +#endif // ANIMONE_ANIMONE_TYPES_H_
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/include/animone/util.h Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,25 @@ +#ifndef ANIMONE_ANIMONE_UTIL_H_ +#define ANIMONE_ANIMONE_UTIL_H_ + +#include <sstream> +#include <string> + +namespace animone::internal::util { + +bool ReadFile(const std::string& path, std::string& data); +bool EqualStrings(const std::string& str1, const std::string& str2); +bool Stem(const std::string& filename, std::string& stem); +bool CheckPattern(const std::string& pattern, const std::string& str); +bool TrimLeft(std::string& str, const char* chars); +bool TrimRight(std::string& str, const char* chars); + +template<typename T = int, std::enable_if_t<std::is_integral<T>::value, bool> = true> +T StringToInt(const std::string& str, T def = 0) { + std::istringstream s(str); + s >> std::noboolalpha >> def; + return def; +} + +} // namespace animone::internal::util + +#endif // ANIMONE_ANIMONE_UTIL_H_
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/include/animone/util/osx.h Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,40 @@ +#ifndef ANIMONE_ANIMONE_UTIL_OSX_H_ +#define ANIMONE_ANIMONE_UTIL_OSX_H_ + +#include "animone/types.h" +#include <cstdint> +#include <string> + +#include <CoreFoundation/CoreFoundation.h> + +namespace animone::internal::osx::util { + +template<typename T> +bool GetCFNumber(CFNumberRef num, T& result) { + if (!num) + return false; + + int64_t res; + if (!CFNumberGetValue(num, static_cast<CFNumberType>(4), &res)) + return false; + + result = static_cast<T>(res); + return true; +} + +template<typename T> +struct CFDeconstructor { + using pointer = T; + void operator()(pointer t) const { ::CFRelease(t); }; +}; + +template<typename T> +using CFPtr = vector<T, CFDecontructor<T>>; // type-id is vector<T, Alloc<T>> + +bool StringFromCFString(CFStringRef string, std::string& result); + +bool GetProcessName(pid_t pid, std::string& result); + +} // namespace animone::internal::osx::util + +#endif // ANIMONE_ANIMONE_UTIL_OSX_H_
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/include/animone/util/win32.h Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,33 @@ +#ifndef ANIMONE_ANIMONE_UTIL_WIN32_H_ +#define ANIMONE_ANIMONE_UTIL_WIN32_H_ + +#include <subauth.h> +#include <windows.h> + +#include <memory> +#include <string> + +namespace animone::internal::win32 { + +struct HandleDeconstructor { + using pointer = HANDLE; + void operator()(pointer t) const { ::CloseHandle(t); }; +}; + +using Handle = std::unique_ptr<HANDLE, HandleDeconstructor>; + +/* ----------------------------------------------- */ + +std::string ToUtf8String(const std::wstring& string); +std::string ToUtf8String(const UNICODE_STRING& string); +std::wstring ToWstring(const std::string& string); + +std::wstring GetFileNameFromPath(const std::wstring& path); +std::wstring GetFileNameWithoutExtension(const std::wstring& filename); + +bool IsSystemDirectory(const std::string& path); +bool IsSystemDirectory(std::wstring path); + +} // namespace animone::internal::win32 + +#endif // ANIMONE_ANIMONE_UTIL_WIN32_H_ \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/include/animone/win.h Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,22 @@ +#ifndef ANIMONE_ANIMONE_WIN_H_ +#define ANIMONE_ANIMONE_WIN_H_ + +#include <functional> +#include <string> + +namespace animone { + +struct Process; +struct Window; + +namespace internal { + +using window_proc_t = std::function<bool(const Process&, const Window&)>; + +bool EnumerateWindows(window_proc_t window_proc); + +} // namespace internal + +} // namespace animone + +#endif // ANIMONE_ANIMONE_WIN_H_
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/include/animone/win/quartz.h Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,12 @@ +#ifndef ANIMONE_ANIMONE_WIN_QUARTZ_H_ +#define ANIMONE_ANIMONE_WIN_QUARTZ_H_ + +#include "animone/win.h" + +namespace animone::internal::quartz { + +bool EnumerateWindows(window_proc_t window_proc); + +} // namespace animone::internal::quartz + +#endif // ANIMONE_ANIMONE_WIN_QUARTZ_H_
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/include/animone/win/win32.h Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,12 @@ +#ifndef ANIMONE_ANIMONE_WIN_WIN32_H_ +#define ANIMONE_ANIMONE_WIN_WIN32_H_ + +#include "animone/win.h" + +namespace animone::internal::win32 { + +bool EnumerateWindows(window_proc_t window_proc); + +} // namespace animone::internal::win32 + +#endif // ANIMONE_ANIMONE_WIN_WIN32_H_
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/include/animone/win/x11.h Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,12 @@ +#ifndef ANIMONE_ANIMONE_WIN_X11_H_ +#define ANIMONE_ANIMONE_WIN_X11_H_ + +#include "animone/win.h" + +namespace animone::internal::x11 { + +bool EnumerateWindows(window_proc_t window_proc); + +} // namespace animone::internal::x11 + +#endif // ANIMONE_ANIMONE_WIN_X11_H_
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/animone.cc Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,76 @@ +#include "animone.h" +#include "animone/fd.h" +#include "animone/strategies.h" +#include "animone/types.h" +#include "animone/util.h" +#include "animone/win.h" + +#include <set> +#include <string> +#include <vector> + +namespace animone { + +namespace internal { + +static bool IsExecutableInList(const Player& player, const std::string& name) { + std::string stem; +#ifdef WIN32 + if (!util::Stem(name, stem)) +#endif + stem = name; + + for (const auto& pattern : player.executables) + if (util::CheckPattern(pattern, stem)) + return true; + + return false; +} + +static bool IsWindowInList(const Player& player, const Window& window) { + for (const auto& pattern : player.windows) + if (util::CheckPattern(pattern, window.class_name)) + return true; + + return false; +} + +static bool PlayerHasStrategy(const Player& player, const Strategy& strategy) { + for (const auto& pstrategy : player.strategies) + if (pstrategy == strategy) + return true; + + return false; +} + +} // namespace internal + +bool GetResults(const std::vector<Player>& players, std::vector<Result>& results) { + auto window_proc = [&](const Process& process, const Window& window) -> bool { + for (const auto& player : players) { + if (internal::IsWindowInList(player, window)) + results.push_back({player, process, window, {}}); + } + + return true; + }; + + if (internal::EnumerateWindows(window_proc)) + return internal::ApplyStrategies(results); + + /* fallback, enumerate over open processes instead */ + auto process_proc = [&](const Process& process) -> bool { + for (const auto& player : players) + if (internal::IsExecutableInList(player, process.name)) + results.push_back({player, process, {}, {}}); + + return true; + }; + + if (internal::EnumerateOpenProcesses(process_proc)) + return internal::ApplyStrategies(results); + + return false; +} + +} // namespace animone
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/fd.cc Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,88 @@ +#include "animone/fd.h" + +#ifdef WIN32 +# include "animone/fd/win32.h" +#endif + +#ifdef LINUX +# include "animone/fd/proc.h" +#endif + +#ifdef MACOSX +# include "animone/fd/xnu.h" +# include "animone/util/osx.h" +#endif + +#ifdef LIBKVM +# include "animone/fd/kvm.h" +#endif + +namespace animone::internal { + +bool EnumerateOpenFiles(const std::set<pid_t>& pids, open_file_proc_t open_file_proc) { + bool success = false; + +#ifdef WIN32 + success ^= win32::EnumerateOpenFiles(pids, open_file_proc); +#endif + +#ifdef LINUX + success ^= proc::EnumerateOpenFiles(pids, open_file_proc); +#endif + +#ifdef MACOSX + success ^= xnu::EnumerateOpenFiles(pids, open_file_proc); +#endif + +#ifdef LIBKVM + success ^= kvm::EnumerateOpenFiles(pids, open_file_proc); +#endif + + return success; +} + +bool EnumerateOpenProcesses(process_proc_t process_proc) { + bool success = false; + +#ifdef WIN32 + success ^= win32::EnumerateOpenProcesses(process_proc); +#endif + +#ifdef LINUX + success ^= proc::EnumerateOpenProcesses(process_proc); +#endif + +#ifdef MACOSX + success ^= xnu::EnumerateOpenProcesses(process_proc); +#endif + +#ifdef LIBKVM + success ^= kvm::EnumerateOpenProcesses(process_proc); +#endif + + return success; +} + +bool GetProcessName(pid_t pid, std::string& name) { + bool success = false; + +#ifdef WIN32 + success ^= win32::GetProcessName(pid, name); +#endif + +#ifdef LINUX + success ^= proc::GetProcessName(pid, name); +#endif + +#ifdef MACOSX + success ^= osx::util::GetProcessName(pid, name); +#endif + +#ifdef LIBKVM + success ^= kvm::GetProcessName(pid, name); +#endif + + return success; +} + +} // namespace animone::internal
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/fd/kvm.cc Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,151 @@ +/* kvm.cc: provides support for *BSD. + */ + +#include "animone/fd/kvm.h" +#include "animone.h" +#include "animone/fd.h" + +#include <sys/file.h> +#include <sys/filedesc.h> +#include <sys/param.h> +#include <sys/queue.h> +#include <sys/sysctl.h> +#include <sys/types.h> +#include <sys/user.h> +#include <sys/vnode.h> + +#include <kvm.h> +#ifdef LIBUTIL +# include <libutil.h> +#endif + +#include <string> + +namespace animone::internal::kvm { + +bool GetProcessName(pid_t pid, std::string& name) { + char errbuf[_POSIX2_LINE_MAX]; + kvm_t* kernel = kvm_openfiles(NULL, NULL, NULL, O_RDONLY, errbuf); + if (!kernel) + return false; + + int entries = 0; + struct kinfo_proc* kinfo = kvm_getprocs(kernel, KERN_PROC_PID, pid, &entries); + if (!kinfo) { + kvm_close(kernel); + return false; + } + + if (entries < 1) { + kvm_close(kernel); + return false; + } + + name = kinfo[0].ki_paddr->p_comm; + + return true; +} + +/* Most of the BSDs share the common kvm library, + * so accessing this information can be trivial. + */ +bool EnumerateOpenProcesses(process_proc_t process_proc) { + char errbuf[_POSIX2_LINE_MAX]; + kvm_t* kernel = kvm_openfiles(NULL, NULL, NULL, O_RDONLY, errbuf); + if (!kernel) + return false; + + int entries = 0; + struct kinfo_proc* kinfo = kvm_getprocs(kernel, KERN_PROC_ALL, 0, &entries); + if (!kinfo) { + kvm_close(kernel); + return false; + } + + for (int i = 0; i < entries; i++) { + if (!process_proc({kinfo[i].ki_paddr->p_pid, kinfo[i].ki_paddr->p_comm})) { + kvm_close(kernel); + return false; + } + } + + kvm_close(kernel); + + return true; +} + +bool EnumerateOpenFiles(std::set<pid_t>& pids, open_file_proc_t open_file_proc) { +#ifdef __OpenBSD__ + char errbuf[_POSIX2_LINE_MAX]; + kvm_t* kernel = kvm_openfiles(NULL, NULL, NULL, O_RDONLY, errbuf); + if (!kernel) + return false; + + for (const auto& pid : pids) { + int cnt; + struct kinfo_file* kfile = kvm_getfiles(kernel, KERN_FILE_BYPID, pid, &cnt); + if (!kfile) { + kvm_close(kernel); + return false; + } + + for (int i = 0; i < cnt; i++) { + if (!open_file_proc({pid, kfile[i].kf_path})) { + kvm_close(kernel); + return false; + } + } + } + + kvm_close(kernel); + + return true; +#elif defined(LIBUTIL) + /* does this code even work? */ + for (const auto& pid : pids) { + int cnt; + std::unique_ptr<struct kinfo_file[]> files(kinfo_getfile(pid, &cnt)); + if (!files) + return false; + + for (int i = 0; i < cnt; i++) { + const struct kinfo_file& current = files[i]; + if (current.kf_vnode_type != KF_VTYPE_VREG) + continue; + + if (!open_file_proc({pid, current.kf_path})) + return false; + } + } + + return true; +#elif defined(__NetBSD__) + for (const auto& pid : pids) { + int mib[6] = {CTL_KERN, KERN_FILE2, KERN_FILE_BYPID, pid, sizeof(struct kinfo_file), 0}; + + size_t len = 0; + if (sysctl(mib, sizeof(mib) / sizeof(mib[0]), NULL, &len, NULL, 0) == -1) + return false; + + mib[5] = len / sizeof(struct kinfo_file); + + std::unique_ptr<struct kinfo_file[]> buf(new struct kinfo_file[mib[5]]); + if (!buf) + return false; + + if (sysctl(mib, sizeof(mib) / sizeof(mib[0]), buf.get(), &len, NULL, 0) == -1) + return false; + + /* TODO: check kfile[i].ki_ofileflags */ + for (size_t i = 0; i < mib[5]; i++) + if (!open_file_proc({pid, kfile[i].kf_path})) + return false; + } + + return true; +#else + return false; +#endif +} + +} // namespace animone::internal::kvm
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/fd/proc.cc Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,138 @@ +#include "animone/fd/proc.h" +#include "animone.h" +#include "animone/util.h" + +#include <filesystem> +#include <fstream> +#include <sstream> +#include <string> + +#include <fcntl.h> +#include <sys/stat.h> +#include <unistd.h> + +static constexpr std::string_view PROC_LOCATION = "/proc"; + +namespace animone::internal::proc { + +static bool IsRegularFile(std::string link) { + struct stat sb; + if (stat(link.c_str(), &sb) == -1) + return false; + + return S_ISREG(sb.st_mode); +} + +static bool AreFlagsOk(pid_t pid, int fd) { + const std::filesystem::path path = + std::filesystem::path(PROC_LOCATION) / std::to_string(pid) / "fdinfo" / std::to_string(fd); + + std::ifstream file(path); + if (!file) + return false; + + int flags = 0; + for (std::string line; std::getline(file, line);) + if (line.find("flags:", 0) == 0) + flags = util::StringToInt(line.substr(line.find_last_not_of("0123456789") + 1)); + + if (flags & O_WRONLY || flags & O_RDWR) + return false; + + return true; +} + +static bool GetFilenameFromFd(std::string link, std::string& out) { + /* /proc is a "virtual filesystem", so we have to guess the path size. yippee! */ + constexpr size_t OUT_MAX = (1ul << 15); // 32KiB + out.resize(32); + + ssize_t exe_used = 0; + do { + out.resize(out.length() * 2); + + exe_used = readlink(link.c_str(), &out.front(), out.length()); + if (exe_used == (ssize_t)-1 || exe_used < (ssize_t)1) + return false; // we got a bad result. SAD! + } while (out.length() < OUT_MAX && exe_used >= static_cast<ssize_t>(out.length())); + + out.resize(out.find('\0')); + + return true; +} + +static bool IsSystemFile(const std::string& path) { + static constexpr std::array<std::string_view, 9> invalid_paths = {"/boot", "/dev", "/bin", "/usr", "/opt", + "/proc", "/var", "/etc", "/dev"}; + + for (const auto& invalid_path : invalid_paths) { + if (!path.rfind(invalid_path, 0)) { + return true; + } + } + + return false; +} + +bool GetProcessName(pid_t pid, std::string& result) { + const std::filesystem::path path = std::filesystem::path(PROC_LOCATION) / std::to_string(pid) / "comm"; + + if (!util::ReadFile(path, result)) + return false; + + result.erase(std::remove(result.begin(), result.end(), '\n'), result.end()); + return true; +} + +bool EnumerateOpenProcesses(process_proc_t process_proc) { + bool success = false; + + for (const auto& dir : std::filesystem::directory_iterator{PROC_LOCATION}) { + Process proc; + + try { + proc.pid = util::StringToInt(dir.path().stem()); + success = true; + } catch (std::invalid_argument const& ex) { + continue; + } + + if (!GetProcessName(proc.pid, proc.name)) + continue; + + if (!process_proc(proc)) + return false; + } + + return success; +} + +bool EnumerateOpenFiles(const std::set<pid_t>& pids, open_file_proc_t open_file_proc) { + if (!open_file_proc) + return false; + + for (const auto& pid : pids) { + const std::filesystem::path path = std::filesystem::path(PROC_LOCATION) / std::to_string(pid) / "fd"; + + for (const auto& dir : std::filesystem::directory_iterator{path}) { + if (!AreFlagsOk(pid, util::StringToInt(dir.path().stem()))) + continue; + + std::string name; + if (!GetFilenameFromFd(dir.path(), name)) + continue; + + if (!IsRegularFile(name)) + continue; + + if (IsSystemFile(name)) + continue; + + if (!open_file_proc({pid, name})) + return false; + } + } + return true; +} + +} // namespace animia::internal::proc
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/fd/win32.cc Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,259 @@ +#include "animone/fd/win32.h" +#include "animone.h" +#include "animone/util/win32.h" + +#include <stdexcept> +#include <string> +#include <unordered_map> +#include <vector> + +#include <fileapi.h> +#include <handleapi.h> +#include <libloaderapi.h> +#include <ntdef.h> +#include <psapi.h> +#include <shlobj.h> +#include <stringapiset.h> +#include <tlhelp32.h> +#include <windows.h> +#include <winternl.h> + +/* This file is noticably more complex than Unix and Linux, and that's because + * there is no "simple" way to get the paths of a file. In fact, this thing requires + * you to use *internal functions* that can't even be linked to, hence why we have to + * use GetProcAddress and such. What a mess. + * + * Speaking of which, because this file uses internal functions of the OS, it is not + * guaranteed to work far into the future. However, it has worked since NT 6.0 (Vista) + * at least, so it's unlikely to be changed much ever. + */ + +/* SystemExtendedHandleInformation is only available in NT 5.1+ (XP and higher) and provides information for + * 32-bit PIDs, unlike SystemHandleInformation + * + * TODO: implement SystemHandleInformation for systems older than XP + */ +static constexpr SYSTEM_INFORMATION_CLASS SystemExtendedHandleInformation = static_cast<SYSTEM_INFORMATION_CLASS>(0x40); +static constexpr SYSTEM_INFORMATION_CLASS SystemHandleInformation = static_cast<SYSTEM_INFORMATION_CLASS>(0x10); +static constexpr NTSTATUS STATUS_INFO_LENGTH_MISMATCH = 0xC0000004UL; + +/* this is filled in at runtime because it's not guaranteed to be (and isn't) + * constant between different versions of Windows */ +static unsigned short file_type_index = 0; + +struct SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX { + PVOID Object; + ULONG_PTR UniqueProcessId; + HANDLE HandleValue; + ACCESS_MASK GrantedAccess; + USHORT CreatorBackTraceIndex; + USHORT ObjectTypeIndex; + ULONG HandleAttributes; + ULONG Reserved; +}; + +struct SYSTEM_HANDLE_INFORMATION_EX { + ULONG_PTR NumberOfHandles; + ULONG_PTR Reserved; + SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX Handles[1]; +}; + +namespace animone::internal::win32 { + +class Ntdll { +public: + Ntdll() { + ntdll = ::GetModuleHandleW(L"ntdll.dll"); + nt_query_system_information = reinterpret_cast<decltype(::NtQuerySystemInformation)*>( + ::GetProcAddress(ntdll, "NtQuerySystemInformation")); + nt_query_object = reinterpret_cast<decltype(::NtQueryObject)*>(::GetProcAddress(ntdll, "NtQueryObject")); + } + + NTSTATUS QuerySystemInformation(SYSTEM_INFORMATION_CLASS cls, PVOID sysinfo, ULONG len, + PULONG retlen){return nt_query_system_information(cls, sysinfo, len, retlen)} + + NTSTATUS QueryObject(HANDLE handle, OBJECT_INFORMATION_CLASS cls, PVOID objinf, ULONG objinflen, PULONG retlen) { + return nt_query_object(handle, cls, objinf, objinflen, retlen); + } + +private: + HMODULE ntdll; + decltype(::NtQuerySystemInformation)* nt_query_system_information; + decltype(::NtQueryObject)* nt_query_object; + +} + +Ntdll ntdll; + +static HANDLE DuplicateHandle(HANDLE process_handle, HANDLE handle) { + HANDLE dup_handle = nullptr; + const bool result = + ::DuplicateHandle(process_handle, handle, ::GetCurrentProcess(), &dup_handle, 0, false, DUPLICATE_SAME_ACCESS); + return result ? dup_handle : nullptr; +} + +static std::vector<SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX> GetSystemHandleInformation() { + /* we should really put a cap on this */ + ULONG cb = 1 << 19; + NTSTATUS status = STATUS_NO_MEMORY; + std::unique_ptr<SYSTEM_HANDLE_INFORMATION_EX> info(malloc(cb)); + + do { + info.reset(malloc(cb *= 2)); + if (!info) + continue; + + status = ntdll.QuerySystemInformation(SystemExtendedHandleInformation, info.get(), cb, &cb); + } while (status == STATUS_INFO_LENGTH_MISMATCH); + + if (!NT_SUCCESS(status)) + return {}; + + std::vector<SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX> res; + + ULONG_PTR handles = info->NumberOfHandles; + if (!handles) + return {}; + + res.reserve(handles); + + SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX* entry = info->Handles; + do { + if (entry) + res.push_back(*(entry++)); + } while (--handles); + + return res; +} + +static std::wstring GetHandleType(HANDLE handle) { + OBJECT_TYPE_INFORMATION info = {0}; + ntdll.QueryObject(handle, ObjectTypeInformation, &info, sizeof(info), NULL); + return std::wstring(info.TypeName.Buffer, info.TypeName.Length); +} + +static std::wstring GetFinalPathNameByHandle(HANDLE handle) { + std::wstring buffer; + + DWORD size = ::GetFinalPathNameByHandleW(handle, NULL, 0, FILE_NAME_NORMALIZED | VOLUME_NAME_DOS); + buffer.resize(size); + ::GetFinalPathNameByHandleW(handle, &buffer.front(), buffer.size(), FILE_NAME_NORMALIZED | VOLUME_NAME_DOS); + + return buffer; +} + +static bool IsFileHandle(HANDLE handle, unsigned short object_type_index) { + if (file_type_index) + return object_type_index == file_type_index; + else if (!handle) + return true; + else if (GetHandleType(handle) == L"File") { + file_type_index = object_type_index; + return true; + } + return false; +} + +static bool IsFileMaskOk(ACCESS_MASK access_mask) { + if (!(access_mask & FILE_READ_DATA)) + return false; + + if ((access_mask & FILE_APPEND_DATA) || (access_mask & FILE_WRITE_EA) || (access_mask & FILE_WRITE_ATTRIBUTES)) + return false; + + return true; +} + +static bool IsFilePathOk(const std::wstring& path) { + if (path.empty()) + return false; + + if (IsSystemDirectory(path)) + return false; + + const auto file_attributes = GetFileAttributesW(path.c_str()); + if ((file_attributes == INVALID_FILE_ATTRIBUTES) || (file_attributes & FILE_ATTRIBUTE_DIRECTORY)) + return false; + + return true; +} + +bool GetProcessName(pid_t pid, std::string& name) { + std::wstring wname = GetProcessPath(); + if (wname.empty()) + return false; + + return ToUtf8String(GetFileNameWithoutExtension(GetFileNameFromPath(wname))); +} + +bool EnumerateOpenProcesses(process_proc_t process_proc) { + HANDLE hProcessSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (hProcessSnap == INVALID_HANDLE_VALUE) + return false; + + PROCESSENTRY32 pe32; + pe32.dwSize = sizeof(PROCESSENTRY32); + + if (!::Process32First(hProcessSnap, &pe32)) + return false; + + if (!process_proc({pe32.th32ProcessID, pe32.szExeFile})) + return false; + + while (::Process32Next(hProcessSnap, &pe32)) + if (!process_proc({pe32.th32ProcessID, pe32.szExeFile})) + return false; + + ::CloseHandle(hProcessSnap); + + return true; +} + +/* this could be changed to being a callback, but... I'm too lazy right now :) */ +bool EnumerateOpenFiles(const std::set<pid_t>& pids, open_file_proc_t open_file_proc) { + if (!open_file_proc) + return false; + + std::unordered_map<pid_t, Handle> proc_handles; + + for (const pid_t& pid : pids) { + const HANDLE handle = ::OpenProcess(PROCESS_DUP_HANDLE, false, pid); + if (handle != INVALID_HANDLE_VALUE) + proc_handles[pid] = Handle(handle); + } + + if (proc_handles.empty()) + return false; + + std::vector<SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX> info = GetSystemHandleInformation(); + + for (const auto& h : info) { + const pid_t pid = h.UniqueProcessId; + if (!pids.count(pid)) + continue; + + if (!IsFileHandle(nullptr, h.ObjectTypeIndex)) + continue; + + if (!IsFileMaskOk(h.GrantedAccess)) + continue; + + Handle handle(DuplicateHandle(proc_handles[pid].get(), h.HandleValue)); + if (handle.get() == INVALID_HANDLE_VALUE) + continue; + + if (GetFileType(handle.get()) != FILE_TYPE_DISK) + continue; + + const std::wstring path = GetFinalPathNameByHandle(handle.get()); + if (!IsFilePathOk(path)) + continue; + + if (!open_file_proc({pid, ToUtf8String(path)})) + return false; + } + + return true; +} + +} // namespace animone::internal::win32
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/fd/xnu.cc Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,84 @@ +#include "animone/fd/xnu.h" +#include "animone.h" +#include "animone/util/osx.h" + +#include <cassert> +#include <memory> +#include <string> +#include <unordered_map> +#include <vector> + +#include <fcntl.h> +#include <libproc.h> +#include <sys/sysctl.h> +#include <sys/types.h> +#include <sys/user.h> + +namespace animone::internal::xnu { + +bool EnumerateOpenProcesses(process_proc_t process_proc) { + size_t pids_size = 256; + std::unique_ptr<pid_t[]> pids; + + int returned_size = 0; + do { + pids.reset(new pid_t[pids_size *= 2]); + returned_size = proc_listpids(PROC_ALL_PIDS, 0, pids.get(), pids_size * sizeof(pid_t)); + if (returned_size == -1) + return false; + } while ((pids_size * sizeof(size_t)) < returned_size); + + for (int i = 0; i < pids_size; i++) { + std::string result; + osx::util::GetProcessName(pids[i], result); + if (!process_proc({pids[i], result})) + return false; + } + + return true; +} + +bool EnumerateOpenFiles(const std::set<pid_t>& pids, open_file_proc_t open_file_proc) { + if (!open_file_proc) + return false; + + for (const auto& pid : pids) { + const int bufsz = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, NULL, 0); + if (bufsz < 0) + return false; + + const size_t info_len = bufsz / sizeof(struct proc_fdinfo); + if (info_len < 1) + return false; + + std::unique_ptr<struct proc_fdinfo[]> info(new struct proc_fdinfo[info_len]); + if (!info) + return false; + + proc_pidinfo(pid, PROC_PIDLISTFDS, 0, info.get(), bufsz); + + for (size_t i = 0; i < info_len; i++) { + if (info[i].proc_fdtype == PROX_FDTYPE_VNODE) { + struct vnode_fdinfowithpath vnodeInfo; + + int sz = proc_pidfdinfo(pid, info[i].proc_fd, PROC_PIDFDVNODEPATHINFO, &vnodeInfo, + PROC_PIDFDVNODEPATHINFO_SIZE); + if (sz != PROC_PIDFDVNODEPATHINFO_SIZE) + return false; + + // This doesn't work (for unknown reasons). I assume somethings fucked up with + // my assumptions; I don't care enough to look into it tbh + // + // if (vnodeInfo.pfi.fi_openflags & O_WRONLY || vnodeInfo.pfi.fi_openflags & O_RDWR) + // continue; + + if (!open_file_proc({pid, vnodeInfo.pvip.vip_path})) + return false; + } + } + } + + return true; +} + +} // namespace animone::internal::xnu
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/player.cc Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,181 @@ +#include "animone/player.h" +#include "animone/util.h" + +#include <map> +#include <sstream> +#include <string> +#include <vector> + +namespace animone { + +namespace internal::parser { + +enum class State { + ExpectPlayerName, + ExpectSection, + ExpectWindow, + ExpectExecutable, + ExpectStrategy, + ExpectType, + ExpectWindowTitle, +}; + +size_t GetIndentation(const std::string& line) { + return line.find_first_not_of('\t'); +} + +bool HandleIndentation(const size_t current, const std::vector<Player>& players, State& state) { + // Each state has a definitive expected indentation + const auto expected = [&state]() -> size_t { + switch (state) { + default: + case State::ExpectPlayerName: return 0; + case State::ExpectSection: return 1; + case State::ExpectWindow: + case State::ExpectExecutable: + case State::ExpectStrategy: + case State::ExpectType: return 2; + case State::ExpectWindowTitle: return 3; + } + }(); + + if (current > expected) + return false; // Disallow excessive indentation + + if (current < expected) { + auto fix_state = [&]() { state = !current ? State::ExpectPlayerName : State::ExpectSection; }; + switch (state) { + case State::ExpectWindow: + if (players.back().windows.empty()) + return false; + fix_state(); + break; + case State::ExpectExecutable: + if (players.back().executables.empty()) + return false; + fix_state(); + break; + case State::ExpectStrategy: + if (players.back().strategies.empty()) + return false; + fix_state(); + break; + case State::ExpectType: fix_state(); break; + case State::ExpectWindowTitle: return false; + } + } + + return true; +} + +bool HandleState(std::string& line, std::vector<Player>& players, State& state) { + switch (state) { + case State::ExpectPlayerName: + players.push_back(Player()); + players.back().name = line; + state = State::ExpectSection; + break; + + case State::ExpectSection: { + static const std::map<std::string, State> sections = { + {"windows", State::ExpectWindow }, + {"executables", State::ExpectExecutable}, + {"strategies", State::ExpectStrategy }, + {"type", State::ExpectType }, + }; + util::TrimRight(line, ":"); + const auto it = sections.find(line); + if (it == sections.end()) + return false; + state = it->second; + break; + } + + case State::ExpectWindow: players.back().windows.push_back(line); break; + + case State::ExpectExecutable: players.back().executables.push_back(line); break; + + case State::ExpectStrategy: { + static const std::map<std::string, Strategy> strategies = { + {"window_title", Strategy::WindowTitle }, + {"open_files", Strategy::OpenFiles }, + {"ui_automation", Strategy::UiAutomation}, + }; + util::TrimRight(line, ":"); + const auto it = strategies.find(line); + if (it == strategies.end()) + return false; + const auto strategy = it->second; + players.back().strategies.push_back(strategy); + switch (strategy) { + case Strategy::WindowTitle: state = State::ExpectWindowTitle; break; + } + break; + } + + case State::ExpectType: { + static const std::map<std::string, PlayerType> types = { + {"default", PlayerType::Default }, + {"web_browser", PlayerType::WebBrowser}, + }; + const auto it = types.find(line); + if (it == types.end()) + return false; + players.back().type = it->second; + break; + } + + case State::ExpectWindowTitle: + players.back().window_title_format = line; + state = State::ExpectStrategy; + break; + } + + return true; +} + +} // namespace internal::parser + +//////////////////////////////////////////////////////////////////////////////// + +bool ParsePlayersData(const std::string& data, std::vector<Player>& players) { + if (data.empty()) + return false; + + std::istringstream stream(data); + std::string line; + size_t indentation = 0; + auto state = internal::parser::State::ExpectPlayerName; + + while (std::getline(stream, line, '\n')) { + if (line.empty()) + continue; // Ignore empty lines + + indentation = internal::parser::GetIndentation(line); + + internal::util::TrimLeft(line, "\t"); + internal::util::TrimRight(line, "\n\r"); + + if (line.empty() || line.front() == '#') + continue; // Ignore empty lines and comments + + if (!internal::parser::HandleIndentation(indentation, players, state)) + return false; + + if (!internal::parser::HandleState(line, players, state)) + return false; + } + + return !players.empty(); +} + +bool ParsePlayersFile(const std::string& path, std::vector<Player>& players) { + std::string data; + + if (!internal::util::ReadFile(path, data)) + return false; + + return ParsePlayersData(data, players); +} + +} // namespace animone
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/strategist.cc Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,111 @@ +#include <regex> + +#include "animone.h" +#include "animone/fd.h" +#include "animone/strategies.h" +#include "animone/util.h" + +#include <iostream> + +/* this was STUPIDLY slow in Anisthesia, oops! */ + +namespace animone::internal { + +static bool ApplyWindowTitleFormat(const std::string& format, std::string& title) { + if (format.empty()) + return false; + + const std::regex pattern(format); + std::smatch match; + std::regex_match(title, match, pattern); + + // Use the first non-empty match result, because the regular expression may + // contain multiple sub-expressions. + for (size_t i = 1; i < match.size(); ++i) { + if (!match.str(i).empty()) { + title = match.str(i); + return true; + } + } + + // Results are empty, but the match was successful + if (!match.empty()) { + title.clear(); + return true; + } + + return true; +} + +static MediaInfoType InferMediaInformationType(const std::string& str) { + const std::regex path_pattern(R"(^(?:[A-Za-z]:[/\\]|\\\\)[^<>:"/\\|?*]+)"); + return (std::regex_search(str, path_pattern)) ? MediaInfoType::File : MediaInfoType::Unknown; +} + +static bool AddMedia(Result& result, const MediaInfo media_information) { + if (media_information.value.empty()) + return false; + + Media media; + media.information.push_back(media_information); + result.media.push_back(std::move(media)); + + return true; +} + +static bool ApplyWindowTitleStrategy(std::vector<Result>& results) { + bool success = false; + + for (auto& result : results) { + auto title = result.window.text; + if (title.empty()) + continue; + + ApplyWindowTitleFormat(result.player.window_title_format, title); + + success |= AddMedia(result, {InferMediaInformationType(title), title}); + } + + return success; +} + +static bool ApplyOpenFilesStrategy(std::vector<Result>& results) { + bool success = false; + + /* map pids to our results, saves time with open_file_proc */ + std::unordered_map<pid_t, Result*> pid_map; + pid_map.reserve(results.size()); + + std::set<pid_t> pids; + + for (Result& result : results) { + const pid_t pid = result.process.pid; + if (!pid) + continue; + + pid_map.insert({pid, &result}); + pids.insert(pid); + } + + auto open_file_proc = [&](const OpenFile& file) -> bool { + success |= AddMedia(*pid_map[file.pid], {MediaInfoType::File, file.path}); + return true; + }; + + EnumerateOpenFiles(pids, open_file_proc); + + return success; +} + +bool ApplyStrategies(std::vector<Result>& results) { + bool success = false; + + success |= ApplyWindowTitleStrategy(results); + success |= ApplyOpenFilesStrategy(results); + + return success; +} + +//////////////////////////////////////////////////////////////////////////////// + +} // namespace animone::internal
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/util.cc Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,85 @@ +#include <algorithm> +#include <fstream> +#include <regex> +#include <sstream> +#include <string> + +#include "animone/util.h" + +namespace animone::internal::util { + +bool ReadFile(const std::string& path, std::string& data) { + std::ifstream file(path.c_str(), std::ios::in | std::ios::binary); + if (!file) + return false; + + std::ostringstream string; + string << file.rdbuf(); + file.close(); + + data = string.str(); + + return true; +} + +/* this assumes ASCII... which really should be the case for what we need, anyway */ +bool EqualStrings(const std::string& str1, const std::string& str2) { + auto tolower = [](const char c) -> char { return ('A' <= c && c <= 'Z') ? c + ('a' - 'A') : c; }; + + auto equal_chars = [&tolower](const char c1, const char c2) -> bool { return tolower(c1) == tolower(c2); }; + + return str1.length() == str2.length() && std::equal(str1.begin(), str1.end(), str2.begin(), equal_chars); +} + +bool Stem(const std::string& filename, std::string& stem) { + unsigned long long pos = filename.find_last_of("."); + if (pos != std::string::npos) + return false; + + stem = filename.substr(0, pos); + return true; +} + +bool CheckPattern(const std::string& pattern, const std::string& str) { + if (pattern.empty()) + return false; + if (pattern.front() == '^' && std::regex_match(str, std::regex(pattern))) + return true; + return util::EqualStrings(pattern, str); +} + +bool TrimLeft(std::string& str, const char* chars) { + if (str.empty()) + return false; + + const auto found = str.find_first_not_of(chars); + + if (found == 0) + return false; + + if (found == std::string::npos) + str.clear(); + else + str.erase(0, found); + + return true; +} + +bool TrimRight(std::string& str, const char* chars) { + if (str.empty()) + return false; + + const auto found = str.find_last_not_of(chars); + + if (found == str.size() - 1) + return false; + + if (found == std::string::npos) + str.clear(); + else + str.resize(found + 1); + + return true; +} + +} // namespace animone::internal::util
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/util/osx.cc Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,196 @@ +#include "animone/util/osx.h" + +#include <memory> +#include <string> + +#include <libproc.h> +#include <sys/sysctl.h> + +namespace animone::internal::osx::util { + +typedef CFTypeRef (*LSASNCreateWithPidSpec)(CFAllocatorRef, pid_t); +typedef CFDictionaryRef (*LSCopyApplicationInformationSpec)(int, CFTypeRef, CFArrayRef); + +/* retrieved dynamically from launchservices */ +static LSCopyApplicationInformationSpec LSCopyApplicationInformation = nullptr; +static LSASNCreateWithPidSpec LSASNCreateWithPid = nullptr; + +static CFStringRef kLSDisplayNameKey = nullptr; +static CFStringRef kLSPIDKey = nullptr; + +/* retrieved from LaunchServicesSPI.h in WebKit */ +static constexpr int kLSDefaultSessionID = -2; + +static const CFStringRef kLaunchServicesBundleID = CFSTR("com.apple.LaunchServices"); + +static bool GetLaunchServicesPrivateSymbols() { + CFBundleRef launch_services_bundle = CFBundleGetBundleWithIdentifier(kLaunchServicesBundleID); + if (!launch_services_bundle) + return false; + + LSCopyApplicationInformation = reinterpret_cast<LSCopyApplicationInformationSpec>( + CFBundleGetFunctionPointerForName(launch_services_bundle, CFSTR("_LSCopyApplicationInformation"))); + if (!LSCopyApplicationInformation) + return false; + + LSASNCreateWithPid = reinterpret_cast<LSASNCreateWithPidSpec>( + CFBundleGetFunctionPointerForName(launch_services_bundle, CFSTR("_LSASNCreateWithPid"))); + if (!LSASNCreateWithPid) + return false; + + CFStringRef* ptr_kLSDisplayNameKey = reinterpret_cast<CFStringRef*>( + CFBundleGetDataPointerForName(launch_services_bundle, CFSTR("_kLSDisplayNameKey"))); + if (!ptr_kLSDisplayNameKey) + return false; + kLSDisplayNameKey = *ptr_kLSDisplayNameKey; + + CFStringRef* ptr_kLSPIDKey = + reinterpret_cast<CFStringRef*>(CFBundleGetDataPointerForName(launch_services_bundle, CFSTR("_kLSPIDKey"))); + if (!ptr_kLSPIDKey) + return false; + kLSPIDKey = *ptr_kLSPIDKey; + + return true; +} + +static bool LaunchServicesGetProcessName(pid_t pid, std::string& result) { + if (!LSCopyApplicationInformation || !LSASNCreateWithPid || !kLSDisplayNameKey || !kLSPIDKey) + if (!GetLaunchServicesPrivateSymbols()) + return false; + + /* what the hell is an `asn`? */ + CFPtr<CFTypeRef> asn = LSASNCreateWithPid(kCFAllocatorDefault, pid); + if (!asn) + return false; + + CFPtr<CFArrayRef> request_array = CFArrayCreate(NULL, (const void**)kLSDisplayNameKey, 1, NULL); + if (!request_array) + return false; + + CFPtr<CFDictionaryRef> dictionary = + LSCopyApplicationInformation(kLSDefaultSessionID, asn.get(), request_array.get()); + if (!dictionary) + return false; + + { + /* this doesn't need to be free'd */ + CFStringRef rstr; + + if (!CFDictionaryGetValueIfPresent(dictionary, kLSDisplayNameKey, (CFTypeRef*)&rstr) || !rstr) + return false; + + if (!StringFromCFString(rstr, result)) + return false; + } + + result.resize(result.find('\0')); + + return true; +} + +bool StringFromCFString(CFStringRef string, std::string& result) { + if (!string) + return false; + + result.resize(CFStringGetMaximumSizeForEncoding(CFStringGetLength(string), kCFStringEncodingUTF8) + 1); + if (!CFStringGetCString(string, &result.front(), result.length(), kCFStringEncodingUTF8)) + return false; + + return true; +} + +static bool GetProcessArgs(pid_t pid, std::string& args) { + /* sysctl shouldn't touch these, so we define them as const */ + int mib[3] = {CTL_KERN, KERN_PROCARGS2, static_cast<int>(pid)}; + const size_t mib_size = sizeof(mib) / sizeof(*mib); + + /* Get the initial size of the array + * + * NOTE: it IS possible for this value to change inbetween calls to sysctl(). + * Unfortunately, I couldn't care less about handling this. :) + * + * is that really true, though? these should be constant values. but are + * argc and argv *really* constant? + */ + size_t size; + { + int ret = sysctl((int*)mib, mib_size, nullptr, &size, nullptr, 0); + if (ret) + return false; + } + + /* Reserve the space for it in args */ + args.resize(size); + + /* Get the contents of argc and argv */ + { + int ret = sysctl((int*)mib, mib_size, &args.front(), &size, NULL, 0); + if (ret) + return false; + } + + /* Is the size big enough to hold at least argc? */ + if (size < sizeof(int)) + return false; + + args.resize(size); + return true; +} + +static bool GetProcessNameFromArgs(pid_t pid, std::string& result) { + if (!GetProcessArgs(pid, result)) + return false; + + /* Get argc using memcpy */ + int argc = 0; + memcpy(&argc, &result.front(), sizeof(argc)); + + /* Do we even have argv[0]? */ + if (argc < 1) + return false; + + /* Find the first null character */ + size_t null_pos = result.find('\0', sizeof(argc)); + if (null_pos == std::string::npos) + return false; + + /* Find the last slash */ + size_t last_slash = result.rfind('/', null_pos); + if (last_slash == std::string::npos) + return false; + + /* Return our result */ + result = result.substr(last_slash + 1, null_pos - last_slash - 1); + return true; +} + +static bool GetProcessNameFromKernel(pid_t pid, std::string& result) { + result.resize(2 * MAXCOMLEN); + + int size = proc_name(pid, &result.front(), result.length()); + if (!size) + return false; + + result.resize(size); + return true; +} + +bool GetProcessName(pid_t pid, std::string& result) { + if (LaunchServicesGetProcessName(pid, result)) + return true; + + /* Try parsing the arguments, this prevents the process name being + * cut off to 2*MAXCOMLEN (32 chars) */ + if (GetProcessNameFromArgs(pid, result)) + return true; + + /* Then attempt getting it from the kernel, which results in the + * process name being cut to 32 chars (worse, 16 chars if p_name is + * unavailable) */ + if (GetProcessNameFromKernel(pid, result)) + return true; + + return false; +} + +} // namespace animone::internal::osx::util
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/util/win32.cc Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,102 @@ +#include "animone/util/win32.h" + +#include <shlobj.h> /* SHGetKnownFolderPath */ +#include <subauth.h> /* UNICODE_STRING */ +#include <windows.h> + +namespace animone::internal::win32 { + +std::string ToUtf8String(const std::wstring& string) { + if (string.empty()) + return std::string(); + + long size = ::WideCharToMultiByte(CP_UTF8, 0, string.c_str(), string.length(), nullptr, 0, nullptr, nullptr); + std::string ret(size, '\0'); + ::WideCharToMultiByte(CP_UTF8, 0, string.c_str(), string.length(), &ret.front(), ret.length(), nullptr, nullptr); + return ret; +} + +std::string ToUtf8String(const UNICODE_STRING& string) { + const auto wctomb = [&string](LPSTR out, int size) -> int { + return ::WideCharToMultiByte(CP_UTF8, 0, string.Buffer, string.Length, out, size, nullptr, nullptr); + }; + + if (string.Length <= 0) + return std::string(); + + long size = wctomb(nullptr, 0); + std::string ret(size, '\0'); + wctomb(&ret.front(), ret.length()); + return ret; +} + +std::wstring ToWstring(const std::string& string) { + const auto mbtowc = [&string](LPWSTR out, int size) -> int { + return ::MultiByteToWideChar(CP_UTF8, 0, string.c_str(), string.length(), out, size); + }; + + if (string.empty()) + return std::wstring(); + + long size = mbtowc(nullptr, 0); + std::wstring ret(size, L'\0'); + mbtowc(&ret.front(), ret.length()); + return ret; +} + +std::wstring GetProcessPath(DWORD process_id) { + // If we try to open a SYSTEM process, this function fails and the last error + // code is ERROR_ACCESS_DENIED. + // + // Note that if we requested PROCESS_QUERY_INFORMATION access right instead + // of PROCESS_QUERY_LIMITED_INFORMATION, this function would fail when used + // to open an elevated process. + Handle process_handle(::OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, process_id)); + + if (!process_handle) + return std::wstring(); + + std::wstring buffer(MAX_PATH, L'\0'); + DWORD buf_size = buffer.length(); + + // Note that this function requires Windows Vista or above. You may use + // GetProcessImageFileName or GetModuleFileNameEx on earlier versions. + if (!::QueryFullProcessImageNameW(process_handle.get(), 0, &buffer.front(), &buf_size)) + return std::wstring(); + + buffer.resize(buf_size); + return buffer; +} + +std::wstring GetFileNameFromPath(const std::wstring& path) { + const auto pos = path.find_last_of(L"/\\"); + return pos != std::wstring::npos ? path.substr(pos + 1) : path; +} + +std::wstring GetFileNameWithoutExtension(const std::wstring& filename) { + const auto pos = filename.find_last_of(L"."); + return pos != std::wstring::npos ? filename.substr(0, pos) : filename; +} + +static std::wstring GetSystemDirectory() { + PWSTR path_wch; + SHGetKnownFolderPath(FOLDERID_Windows, 0, NULL, &path_wch); + std::wstring path_wstr(path_wch); + CoTaskMemFree(path_wch); + return path_wstr; +} + +bool IsSystemDirectory(const std::string& path) { + return IsSystemDirectory(ToWstring(path)); +} + +bool IsSystemDirectory(std::wstring path) { + ::CharUpperBuffW(&path.front(), path.length()); + + std::wstring windir = GetSystemDirectory(); + ::CharUpperBuffW(&windir.front(), windir.length()); + + return path.find(windir) == 4; +} + +} // namespace animone::internal::win32
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/win.cc Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,35 @@ +#include "animone/win.h" + +#ifdef WIN32 +# include "animone/win/win32.h" +#endif + +#ifdef MACOSX +# include "animone/win/quartz.h" +#endif + +#ifdef X11 +# include "animone/win/x11.h" +#endif + +namespace animone::internal { + +bool EnumerateWindows(window_proc_t window_proc) { + bool success = false; + +#ifdef WIN32 + success |= win32::EnumerateWindows(window_proc); +#endif + +#ifdef MACOSX + success |= quartz::EnumerateWindows(window_proc); +#endif + +#ifdef X11 + success |= x11::EnumerateWindows(window_proc); +#endif + + return success; +} + +} // namespace animone::internal
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/win/quartz.cc Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,251 @@ +/* + * win/quartz.cc: support for macOS (the Quartz Compositor) + * + * This file does not require an Objective-C++ compiler, + * but it *does* require an Objective-C runtime. + */ +#include "animone/win/quartz.h" +#include "animone.h" +#include "animone/util/osx.h" + +#include <objc/message.h> +#include <objc/runtime.h> + +#include <ApplicationServices/ApplicationServices.h> +#include <CoreFoundation/CoreFoundation.h> +#include <CoreGraphics/CoreGraphics.h> + +namespace animone::internal::quartz { + +#if __LP64__ +typedef long NSInteger; +#else +typedef int NSInteger; +#endif +typedef int CGSConnection; + +typedef CGSConnection (*CGSDefaultConnectionForThreadSpec)(void); +typedef CGError (*CGSCopyWindowPropertySpec)(const CGSConnection, NSInteger, CFStringRef, CFStringRef*); + +static CGSDefaultConnectionForThreadSpec CGSDefaultConnectionForThread = nullptr; +static CGSCopyWindowPropertySpec CGSCopyWindowProperty = nullptr; + +static const CFStringRef kCoreGraphicsBundleID = CFSTR("com.apple.CoreGraphics"); + +/* Objective-C */ +typedef id (*object_message_send)(id, SEL, ...); +typedef id (*class_message_send)(Class, SEL, ...); + +static const object_message_send obj_send = reinterpret_cast<object_message_send>(objc_msgSend); +static const class_message_send cls_send = reinterpret_cast<class_message_send>(objc_msgSend); + +static bool GetCoreGraphicsPrivateSymbols() { + CFBundleRef core_graphics_bundle = CFBundleGetBundleWithIdentifier(kCoreGraphicsBundleID); + if (!core_graphics_bundle) + return false; + + CGSDefaultConnectionForThread = (CGSDefaultConnectionForThreadSpec)CFBundleGetFunctionPointerForName( + core_graphics_bundle, CFSTR("CGSDefaultConnectionForThread")); + if (!CGSDefaultConnectionForThread) + return false; + + CGSCopyWindowProperty = (CGSCopyWindowPropertySpec)CFBundleGetFunctionPointerForName( + core_graphics_bundle, CFSTR("CGSCopyWindowProperty")); + if (!CGSCopyWindowProperty) + return false; + + return true; +} + +template<typename T> +static bool CFDictionaryGetValue(CFDictionaryRef thedict, CFStringRef key, T& out) { + CFTypeRef data = nullptr; + if (!CFDictionaryGetValueIfPresent(thedict, key, reinterpret_cast<const void**>(&data)) || !data) + return false; + + if constexpr (std::is_arithmetic<T>::value) + osx::util::GetCFNumber(reinterpret_cast<CFNumberRef>(data), out); + else if constexpr (std::is_same<T, std::string>::value) + osx::util::StringFromCFString(reinterpret_cast<CFStringRef>(data), out); + else + return false; + + return true; +} + +static bool GetWindowTitleAccessibility(unsigned int wid, pid_t pid, std::string& result) { + CGRect bounds = {0}; + { + const CGWindowID wids[1] = {wid}; + CFPtr<CFArrayRef> arr(CFArrayCreate(kCFAllocatorDefault, (CFTypeRef*)wids, 1, NULL)); + CFPtr<CFArrayRef> dicts(CGWindowListCreateDescriptionFromArray(arr)); + + if (!dicts.get() || CFArrayGetCount(dicts.get()) < 1) + return false; + + CFDictionaryRef dict = reinterpret_cast<CFDictionaryRef>(CFArrayGetValueAtIndex(dicts, 0)); + if (!dict) + return false; + + CFDictionaryRef bounds_dict = nullptr; + if (!CFDictionaryGetValueIfPresent(dict, kCGWindowBounds, reinterpret_cast<CFTypeRef*>(&bounds_dict)) || + !bounds_dict) + return false; + + if (!CGRectMakeWithDictionaryRepresentation(bounds_dict, &bounds)) + return false; + } + + /* now we can actually do stuff */ + AXUIElementRef axapp = AXUIElementCreateApplication(pid); + CFPtr<CFArrayRef> windows; + { + CFArrayRef ref; + if ((AXUIElementCopyAttributeValue(axapp, kAXWindowsAttribute, reinterpret_cast<CFTypeRef*>(&ref)) != + kAXErrorSuccess) || + !windows) + return false; + + windows.reset(ref); + } + + const CFIndex count = CFArrayGetCount(windows.get()); + for (CFIndex i = 0; i < count; i++) { + const AXUIElementRef window = reinterpret_cast<AXUIElementRef>(CFArrayGetValueAtIndex(windows.get(), i)); + + /* does this leak memory? probably. */ + AXValueRef val; + if (AXUIElementCopyAttributeValue(window, kAXPositionAttribute, reinterpret_cast<CFTypeRef*>(&val)) == + kAXErrorSuccess) { + CGPoint point; + if (!AXValueGetValue(val, kAXValueTypeCGPoint, reinterpret_cast<CFTypeRef>(&point)) || + (point.x != bounds.origin.x || point.y != bounds.origin.y)) + continue; + } else + continue; + + if (AXUIElementCopyAttributeValue(window, kAXSizeAttribute, reinterpret_cast<CFTypeRef*>(&val)) == + kAXErrorSuccess) { + CGSize size; + if (!AXValueGetValue(val, kAXValueTypeCGSize, reinterpret_cast<CFTypeRef>(&size)) || + (size.width != bounds.size.width || size.height != bounds.size.height)) + continue; + } else + continue; + + CFStringRef title; + if (AXUIElementCopyAttributeValue(window, kAXTitleAttribute, reinterpret_cast<CFTypeRef*>(&title)) == + kAXErrorSuccess) + return osx::util::StringFromCFString(title, result); + } + + return false; +} + +static bool GetWindowTitle(unsigned int wid, pid_t pid, std::string& result) { + /* try using CoreGraphics (only usable on old versions of OS X) */ + if ((CGSDefaultConnectionForThread && CGSCopyWindowProperty) || GetCoreGraphicsPrivateSymbols()) { + CFPtr<CFStringRef> title; + { + CFStringRef t = nullptr; + CGSCopyWindowProperty(CGSDefaultConnectionForThread(), wid, CFSTR("kCGSWindowTitle"), &t); + title.reset(t); + } + + if (title && CFStringGetLength(title.get()) && osx::util::StringFromCFString(title.get(), result)) + return true; + } + + /* then try linking to a window using the accessibility API */ + return AXIsProcessTrusted() ? GetWindowTitleAccessibility(wid, pid, result) : false; +} + +static bool GetProcessBundleIdentifierNew(pid_t pid, std::string& result) { + /* 10.6 and higher */ + const id app = + cls_send(objc_getClass("NSRunningApplication"), sel_getUid("runningApplicationWithProcessIdentifier:"), pid); + if (!app) + return false; + + CFStringRef bundle_id = reinterpret_cast<CFStringRef>(obj_send(app, sel_getUid("bundleIdentifier"))); + if (!bundle_id) + return false; + + result = osx::util::StringFromCFString(bundle_id, result); + return true; +} + +static bool GetProcessBundleIdentifierOld(pid_t pid, std::string& result) { + /* OS X 10.2; deprecated in 10.9 */ + ProcessSerialNumber psn; + if (GetProcessForPID(pid, &psn)) + return false; + + CFPtr<CFDictionaryRef> info = ProcessInformationCopyDictionary(psn, kProcessDictionaryIncludeAllInformationMask); + if (!info) + return false; + + CFStringRef value = reinterpret_cast<CFStringRef>(CFDictionaryGetValue(dict, CFSTR("CFBundleIdentifier"))); + if (!value) + return false; + + result = osx::util::StringFromCFString(value, result); + return true; +} + +static bool GetProcessBundleIdentifier(pid_t pid, std::string& result) { + /* The Bundle ID is essentially OS X's solution to Windows' + * "class name"; theoretically, it should be different for + * each program, although it requires an app bundle. + */ + if (GetProcessBundleIdentifierNew(pid, result)) + return true; + + return GetProcessBundleIdentifierOld(); +} + +bool EnumerateWindows(window_proc_t window_proc) { + if (!window_proc) + return false; + + const CFArrayRef windows = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID); + if (!windows) + return false; + + const CFIndex count = CFArrayGetCount(windows); + for (CFIndex i = 0; i < count; i++) { + CFDictionaryRef window = reinterpret_cast<CFDictionaryRef>(CFArrayGetValueAtIndex(windows, i)); + if (!window) + continue; + + Process proc; + { + CFDictionaryGetValue(window, CFSTR("kCGWindowOwnerPID"), proc.pid); + if (!CFDictionaryGetValue(window, CFSTR("kCGWindowOwnerName"), proc.name)) + osx::util::GetProcessName(proc.pid, proc.name); + } + + Window win; + { + CFDictionaryGetValue(window, CFSTR("kCGWindowNumber"), win.id); + + if (!GetProcessBundleIdentifier(proc.pid, win.class_name)) + // Fallback to the Quartz window name, which is unlikely to be filled, but it + // *could* be. + CFDictionaryGetValue(window, CFSTR("kCGWindowName"), win.class_name); + + GetWindowTitle(win.id, proc.pid, win.text); + } + + if (!window_proc(proc, win)) { + CFRelease(windows); + return false; + } + } + + CFRelease(windows); + + return true; +} + +} // namespace animone::internal::quartz
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/win/win32.cc Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,143 @@ +/* + * win/win32.cc: support for Windows + * + * Surprisingly, this is the one time where Microsoft actually + * does it fairly OK. Everything has a pretty simple API, despite + * the stupid wide string stuff. + */ +#include "animone/win/win32.h" +#include "animone.h" +#include "animone/util/win32.h" +#include "animone/win.h" + +#include <set> +#include <string> + +#include <windows.h> + +namespace animone::internal::win32 { + +static std::wstring GetWindowClassName(HWND hwnd) { + static constexpr int kMaxSize = 256; + + std::wstring buffer(kMaxSize, L'\0'); + const auto size = ::GetClassNameW(hwnd, &buffer.front(), buffer.length()); + buffer.resize(size); + return buffer; +} + +static std::wstring GetWindowText(HWND hwnd) { + const auto estimated_size = ::GetWindowTextLengthW(hwnd); + std::wstring buffer(estimated_size + 1, L'\0'); + + const auto size = ::GetWindowTextW(hwnd, &buffer.front(), buffer.length()); + /* GetWindowTextLength docs: + * "Under certain conditions, the GetWindowTextLength function may return a value + * that is larger than the actual length of the text." */ + buffer.resize(size); + return buffer; +} + +static DWORD GetWindowProcessId(HWND hwnd) { + DWORD process_id = 0; + ::GetWindowThreadProcessId(hwnd, &process_id); + return process_id; +} + +//////////////////////////////////////////////////////////////////////////////// + +static bool VerifyWindowStyle(HWND hwnd) { + const auto window_style = ::GetWindowLong(hwnd, GWL_STYLE); + const auto window_ex_style = ::GetWindowLong(hwnd, GWL_EXSTYLE); + + auto has_style = [&window_style](DWORD style) { return (window_style & style) != 0; }; + auto has_ex_style = [&window_ex_style](DWORD ex_style) { return (window_ex_style & ex_style) != 0; }; + + // Toolbars, tooltips and similar topmost windows + if (has_style(WS_POPUP) && has_ex_style(WS_EX_TOOLWINDOW)) + return false; + if (has_ex_style(WS_EX_TOPMOST) && has_ex_style(WS_EX_TOOLWINDOW)) + return false; + + return true; +} + +static bool VerifyClassName(const std::wstring& name) { + static const std::set<std::wstring> invalid_names = { + // System classes + L"#32770", // Dialog box + L"CabinetWClass", // Windows Explorer + L"ComboLBox", + L"DDEMLEvent", + L"DDEMLMom", + L"DirectUIHWND", + L"GDI+ Hook Window Class", + L"IME", + L"Internet Explorer_Hidden", + L"MSCTFIME UI", + L"tooltips_class32", + }; + + return !name.empty() && !invalid_names.count(name); +} + +static bool VerifyProcessPath(const std::wstring& path) { + return !path.empty() && !IsSystemDirectory(path); +} + +static bool VerifyProcessFileName(const std::wstring& name) { + static const std::set<std::wstring> invalid_names = { + // System files + L"explorer", // Windows Explorer + L"taskeng", // Task Scheduler Engine + L"taskhost", // Host Process for Windows Tasks + L"taskhostex", // Host Process for Windows Tasks + L"Taskmgr", // Task Manager + }; + + return !name.empty() && !invalid_names.count(name); +} + +//////////////////////////////////////////////////////////////////////////////// + +static BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM param) { + if (!::IsWindowVisible(hwnd)) + return TRUE; + + if (!VerifyWindowStyle(hwnd)) + return TRUE; + + Window window; + window.id = static_cast<unsigned int>(reinterpret_cast<ULONG_PTR>(hwnd)); + window.text = ToUtf8String(GetWindowText(hwnd)); + + { + std::wstring class_name = GetWindowClassName(hwnd); + window.class_name = ToUtf8String(class_name); + if (!VerifyClassName(class_name)) + return TRUE; + } + + Process process; + process.pid = GetWindowProcessId(hwnd); + process.name = fd::GetProcessName(process.pid) + + auto& window_proc = *reinterpret_cast<window_proc_t*>(param); + if (!window_proc(process, window)) + return FALSE; + + return TRUE; +} + +bool EnumerateWindows(window_proc_t window_proc) { + if (!window_proc) + return false; + + const auto param = reinterpret_cast<LPARAM>(&window_proc); + + // Note that EnumWindows enumerates only top-level windows of desktop apps + // (as opposed to UWP apps) on Windows 8 and above. + return ::EnumWindows(EnumWindowsProc, param) != FALSE; +} + +} // namespace animone::internal::win32
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/win/x11.cc Mon Apr 01 02:43:44 2024 -0400 @@ -0,0 +1,255 @@ +#include "animone/win/x11.h" +#include "animone.h" +#include "animone/fd.h" /* GetProcessName() */ +#include "animone/win.h" + +#include <xcb/res.h> +#include <xcb/xcb.h> + +#include <climits> +#include <cstdint> +#include <cstring> +#include <set> +#include <string> + +#include <chrono> + +#include <iostream> + +/* This uses XCB (and it uses it *right*), so it should be plenty fast */ + +static size_t str_nlen(const char* s, size_t len) { + size_t i = 0; + for (; i < len && s[i]; i++) + ; + return i; +} + +namespace animone::internal::x11 { + +static bool GetAllTopLevelWindowsEWMH(xcb_connection_t* connection, const std::vector<xcb_window_t>& roots, + std::set<xcb_window_t>& result) { + const xcb_atom_t Atom__NET_CLIENT_LIST = [connection] { + static constexpr std::string_view name = "_NET_CLIENT_LIST"; + xcb_intern_atom_cookie_t cookie = ::xcb_intern_atom(connection, true, name.size(), name.data()); + std::unique_ptr<xcb_intern_atom_reply_t> reply(::xcb_intern_atom_reply(connection, cookie, NULL)); + + xcb_atom_t atom = reply->atom; + + return atom; + }(); + if (Atom__NET_CLIENT_LIST == XCB_ATOM_NONE) + return false; // BTFO + + bool success = false; + + std::vector<xcb_get_property_cookie_t> cookies; + cookies.reserve(roots.size()); + + for (const auto& root : roots) + cookies.push_back(::xcb_get_property(connection, 0, root, Atom__NET_CLIENT_LIST, XCB_ATOM_ANY, 0L, UINT_MAX)); + + for (const auto& cookie : cookies) { + std::unique_ptr<xcb_get_property_reply_t> reply(::xcb_get_property_reply(connection, cookie, NULL)); + + if (reply) { + xcb_window_t* value = reinterpret_cast<xcb_window_t*>(::xcb_get_property_value(reply)); + int len = ::xcb_get_property_value_length(reply); + + for (size_t i = 0; i < len; i++) + result.insert(value[i]); + + success |= true; + } + } + + return success; +} + +/* This is called on every window. What this does is: + * 1. Gets the tree of children + * 2. Searches all children recursively for a WM_STATE property + * 3. If that failed... return the original window + */ +static bool WalkWindows(xcb_connection_t* connection, int depth, xcb_atom_t Atom_WM_STATE, const xcb_window_t* windows, + int windows_len, std::set<xcb_window_t>& result) { + /* The depth we should start returning at. */ + static constexpr int CUTOFF = 2; + + bool success = false; + + std::vector<xcb_query_tree_cookie_t> cookies; + cookies.reserve(windows_len); + + for (int i = 0; i < windows_len; i++) + cookies.push_back(::xcb_query_tree(connection, windows[i])); + + for (const auto& cookie : cookies) { + std::unique_ptr<xcb_query_tree_reply_t> query_tree_reply(::xcb_query_tree_reply(connection, cookie, NULL)); + + xcb_window_t* tree_children = ::xcb_query_tree_children(query_tree_reply); + int tree_children_len = ::xcb_query_tree_children_length(query_tree_reply); + + std::vector<xcb_get_property_cookie_t> state_property_cookies; + state_property_cookies.reserve(tree_children_len); + + for (int i = 0; i < tree_children_len; i++) + state_property_cookies.push_back( + ::xcb_get_property(connection, 0, tree_children[i], Atom_WM_STATE, Atom_WM_STATE, 0, 0)); + + for (int i = 0; i < tree_children_len; i++) { + std::unique_ptr<xcb_get_property_reply_t> get_property_reply( + ::xcb_get_property_reply(connection, state_property_cookies[i], NULL)); + + /* X11 is unfriendly here. what this means is "did the property exist?" */ + if (get_property_reply->format || get_property_reply->type || get_property_reply->length) { + result.insert(tree_children[i]); + if (depth >= CUTOFF) + return true; + + success |= true; + continue; + } + } + + if (WalkWindows(connection, depth + 1, Atom_WM_STATE, tree_children, tree_children_len, result)) { + success |= true; + if (depth >= CUTOFF) + return true; + continue; + } + } + + return success; +} + +static bool GetAllTopLevelWindowsICCCM(xcb_connection_t* connection, const std::vector<xcb_window_t>& roots, + std::set<xcb_window_t>& result) { + bool success = false; + + xcb_atom_t Atom_WM_STATE = [connection] { + static constexpr std::string_view name = "WM_STATE"; + xcb_intern_atom_cookie_t cookie = ::xcb_intern_atom(connection, true, name.size(), name.data()); + xcb_intern_atom_reply_t* reply = ::xcb_intern_atom_reply(connection, cookie, NULL); + + xcb_atom_t atom = reply->atom; + free(reply); + return atom; + }(); + if (Atom_WM_STATE == XCB_ATOM_NONE) + return success; + + std::vector<xcb_query_tree_cookie_t> cookies; + cookies.reserve(roots.size()); + + for (const auto& root : roots) + cookies.push_back(::xcb_query_tree(connection, root)); + + for (const auto& cookie : cookies) + success |= WalkWindows(connection, 0, Atom_WM_STATE, roots.data(), roots.size(), result); + + return success; +} + +bool EnumerateWindows(window_proc_t window_proc) { + if (!window_proc) + return false; + + xcb_connection_t* connection = ::xcb_connect(NULL, NULL); + if (!connection) + return false; + + std::set<xcb_window_t> windows; + { + std::vector<xcb_window_t> roots; + { + xcb_screen_iterator_t iter = ::xcb_setup_roots_iterator(xcb_get_setup(connection)); + for (; iter.rem; ::xcb_screen_next(&iter)) + roots.push_back(iter.data->root); + } + + if (!GetAllTopLevelWindowsEWMH(connection, roots, windows)) + GetAllTopLevelWindowsICCCM(connection, roots, windows); + } + + struct WindowCookies { + xcb_window_t window; + xcb_get_property_cookie_t class_property_cookie; + xcb_get_property_cookie_t name_property_cookie; + xcb_res_query_client_ids_cookie_t pid_property_cookie; + }; + + std::vector<WindowCookies> window_cookies; + window_cookies.reserve(windows.size()); + + for (const auto& window : windows) { + xcb_res_client_id_spec_t spec = {.client = window, .mask = XCB_RES_CLIENT_ID_MASK_LOCAL_CLIENT_PID}; + + WindowCookies window_cookie = { + window, ::xcb_get_property(connection, 0, window, XCB_ATOM_WM_CLASS, XCB_ATOM_STRING, 0L, 2048L), + ::xcb_get_property(connection, 0, window, XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 0L, UINT_MAX), + ::xcb_res_query_client_ids(connection, 1, &spec)}; + + window_cookies.push_back(window_cookie); + } + + for (const auto& window_cookie : window_cookies) { + Window win = {0}; + win.id = window_cookie.window; + { + /* Class name */ + std::unique_ptr<xcb_get_property_reply_t> reply( + ::xcb_get_property_reply(connection, window_cookie.class_property_cookie, NULL)); + + if (reply && reply->format == 8) { + const char* data = reinterpret_cast<char*>(::xcb_get_property_value(reply.get())); + const int data_len = ::xcb_get_property_value_length(reply.get()); + + int instance_len = str_nlen(data, data_len); + const char* class_name = data + instance_len + 1; + + win.class_name = std::string(class_name, str_nlen(class_name, data_len - (instance_len + 1))); + } + } + { + /* Title text */ + std::unique_ptr<xcb_get_property_reply_t> reply( + ::xcb_get_property_reply(connection, window_cookie.name_property_cookie, NULL)); + + if (reply) { + const char* data = reinterpret_cast<char*>(::xcb_get_property_value(reply.get())); + int len = ::xcb_get_property_value_length(reply.get()); + + win.text = std::string(data, len); + } + } + Process proc = {0}; + { + /* PID */ + std::unique_ptr<xcb_res_query_client_ids_reply_t> reply( + ::xcb_res_query_client_ids_reply(connection, window_cookie.pid_property_cookie, NULL)); + + if (reply) { + xcb_res_client_id_value_iterator_t it = ::xcb_res_query_client_ids_ids_iterator(reply); + for (; it.rem; ::xcb_res_client_id_value_next(&it)) { + if (it.data->spec.mask & XCB_RES_CLIENT_ID_MASK_LOCAL_CLIENT_PID) { + proc.pid = *reinterpret_cast<uint32_t*>(::xcb_res_client_id_value_value(it.data)); + fd::GetProcessName(proc.pid, proc.name); /* fill this in if we can */ + break; + } + } + } + } + + if (!window_proc(proc, win)) { + ::xcb_disconnect(connection); + return false; + } + } + + ::xcb_disconnect(connection); + + return true; +} + +} // namespace animone::internal::x11