mirror of
https://github.com/fastfetch-cli/fastfetch.git
synced 2026-10-29 11:10:59 +01:00
Compare commits
149 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 17bb3fd238 | |||
| 1d8bbeb89f | |||
| 939bfbb844 | |||
| fdd4a60d9d | |||
| 6eb3a8148b | |||
| 4f6a329ea3 | |||
| cb73fdcfc9 | |||
| 8a0c1d2f6c | |||
| 2aa053a904 | |||
| a9ee641bd6 | |||
| f93ffed57a | |||
| 73ebe95120 | |||
| b7bc8ca99d | |||
| 1bab113d25 | |||
| 3b042d26ab | |||
| 29567e1674 | |||
| 1e8b4eb974 | |||
| 3453de61f8 | |||
| ad1d12ed38 | |||
| ab6ec4c3f5 | |||
| 3ccaf2d7dd | |||
| 1f32e0bcb2 | |||
| d7b66c8677 | |||
| 7f9faeb720 | |||
| cc435d2b70 | |||
| e0361fdcf6 | |||
| 124f474f81 | |||
| 068d86ef13 | |||
| 10d6b3a3d7 | |||
| 62617a35de | |||
| 10379d600d | |||
| de414b7250 | |||
| be51ba9f15 | |||
| 132e69b580 | |||
| 07a7211526 | |||
| 0806145f4d | |||
| a7e19308cc | |||
| 262a8ca4d3 | |||
| 30d6fb542b | |||
| 1e0ec1004a | |||
| 35a35f066c | |||
| 9859b248ff | |||
| 66709ccc77 | |||
| 10ca4f171e | |||
| 04a5c2d9d3 | |||
| 4909ac0d52 | |||
| fb89af3d84 | |||
| 2bcd49c106 | |||
| 3a069b2331 | |||
| d5e50ee30c | |||
| 845d5983aa | |||
| 1989c6f549 | |||
| 6e2a25759d | |||
| 460a715901 | |||
| 9ac3822105 | |||
| e02e44f673 | |||
| 0f712b4eaf | |||
| 995d172d3e | |||
| 8102478f2f | |||
| 537ffac39c | |||
| 95bcc4da5b | |||
| 1b3594f600 | |||
| ae1288d087 | |||
| b201fd2db9 | |||
| 7f458b03e8 | |||
| 5f65515c3c | |||
| 3d0747b56d | |||
| 68c5dd31f5 | |||
| a57ebe992a | |||
| 54dcb1b6f9 | |||
| 39d667d1d5 | |||
| 7099964530 | |||
| ae4cbcbdba | |||
| cee27354ba | |||
| 109644ec77 | |||
| 782b7b8fd1 | |||
| a9bf53b4a1 | |||
| ab200636f1 | |||
| 4d8d6a5fe9 | |||
| ef853fe858 | |||
| 47fbfcb0e9 | |||
| c88ec3bcc3 | |||
| 2838dccbfd | |||
| f0d8c996e6 | |||
| 6ff7925f45 | |||
| ffbf4ec7ac | |||
| a3f46d9742 | |||
| 4fb57bb39b | |||
| cdbe8f54c6 | |||
| 80632920bb | |||
| 5246f149eb | |||
| f00a021e27 | |||
| 676dbcfee2 | |||
| 9801d89316 | |||
| fb52d32452 | |||
| 36dc82e926 | |||
| 7fdd1d8e93 | |||
| 0c3b852bf7 | |||
| 866c5e89c8 | |||
| a1edf7316c | |||
| c3164d15e1 | |||
| a2eda0090a | |||
| d8797ae314 | |||
| 93175aea1d | |||
| 537635cd9c | |||
| c68bedd9bf | |||
| 80a931a87f | |||
| d223b04ad8 | |||
| 0e04ebdf63 | |||
| bba45429ae | |||
| 00ccd8be12 | |||
| 42e4fe3085 | |||
| fb5622215b | |||
| ccf1dcfa6d | |||
| 0c6a9d6e98 | |||
| b6b5dabd54 | |||
| fdbaaea962 | |||
| add2339c9c | |||
| e1e2de0929 | |||
| 59f0f533e7 | |||
| 28f4f6464b | |||
| 982313063d | |||
| 1e7386788f | |||
| 3a9595e121 | |||
| f8df95d146 | |||
| 592ef68d60 | |||
| 0bf6c8df74 | |||
| be15a0942c | |||
| 4a97cc6825 | |||
| a78d2a7737 | |||
| 3930759007 | |||
| e21530551d | |||
| c26bace3c2 | |||
| fe49a9624e | |||
| 7f3209181e | |||
| 2f74d6be4d | |||
| 256c6a4257 | |||
| aaa9153f5a | |||
| f26795826c | |||
| 18b31ca875 | |||
| 17b98007b4 | |||
| 1023388d60 | |||
| 6842ae0757 | |||
| b726ee5a85 | |||
| fd08fdb9df | |||
| 203ccb31fa | |||
| 3503facd68 | |||
| b2d48a1908 | |||
| b803d1ac82 |
@@ -1,5 +0,0 @@
|
||||
[codespell]
|
||||
check-filenames =
|
||||
builtin = clear,rare,usage,informal
|
||||
skip = */.git,*/cmake-build-*,*/.idea,*/completions,*/presets,*/screenshots,*/tests,*/3rdparty,*/logo/ascii,./src/detection/gpu/asahi_drm.h,./src/detection/cpu/cpu_x86.c,./src/detection/gpu/gpu_bsddrm.c,./src/detection/displayserver/linux/wayland/*-protocol.*,./build,./build-*,./build_*,./src/common/impl/genconfig.c
|
||||
ignore-words-list = iterm,compiletime,unknwn,pengwin,siduction,master,slave,sur,doas,conexant,ags,bu,renderD,SME,Carmel,kinda,symbole,connexion,comando,processus,lokale,adres,titel,som,environnement,Sistem,Lokal,Lokal,Sistem,Paket,Hava,statics
|
||||
@@ -1,2 +1,15 @@
|
||||
# Normalise text files to LF in the repository and check them out with LF, so a CRLF
|
||||
# never enters an object and never shows up as a whole-file diff on Windows.
|
||||
* text=auto eol=lf
|
||||
|
||||
# The vendored AMD Display Library SDK ships with CRLF endings of its own. Leaving it
|
||||
# alone keeps `Dep (3rdparty): upgrades display-library` from carrying a 7000-line
|
||||
# whitespace diff, and a re-vendor will not fight the rule above.
|
||||
src/3rdparty/display-library/** -text
|
||||
|
||||
# The logo fixtures store the exact bytes fastfetch prints, ANSI escapes included.
|
||||
# They are demo data rather than text, and are not read by any test.
|
||||
*.fflogo -text
|
||||
|
||||
*.h linguist-language=c
|
||||
*.c linguist-language=c
|
||||
|
||||
@@ -59,13 +59,14 @@
|
||||
.legend-item.is-hidden .legend-swatch { opacity: .35; }
|
||||
.chart-tooltip { position: fixed; z-index: 1000; pointer-events: none; width: min(90vw, 900px); max-height: calc(100vh - 24px); overflow: hidden; padding: .7rem .85rem; border: 1px solid var(--border); border-radius: .6rem; background: var(--card); color: var(--text); box-shadow: 0 10px 30px #0f172a33; font-size: .8rem; opacity: 0; transition: opacity .1s ease; }
|
||||
.chart-tooltip-title { margin-bottom: .35rem; font-weight: 700; white-space: normal; overflow-wrap: anywhere; }
|
||||
.chart-tooltip-message { margin-bottom: .35rem; color: var(--muted); font-weight: 400; white-space: pre-wrap; overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.chart-tooltip-body { max-height: calc(100vh - 76px); overflow: auto; columns: 220px auto; column-gap: 1.5rem; }
|
||||
.chart-tooltip-row { display: flex; align-items: center; gap: .45rem; break-inside: avoid; white-space: nowrap; }
|
||||
.chart-tooltip-swatch { width: .55rem; height: .55rem; border-radius: 50%; flex: 0 0 auto; }
|
||||
.empty { padding: 2rem 0; color: var(--muted); }
|
||||
</style>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js/dist/chart.umd.min.js"></script>
|
||||
<script src="data.js"></script>
|
||||
<script src="https://unpkg.com/chart.js@latest/dist/chart.umd.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
@@ -73,11 +74,11 @@
|
||||
<h1>Fastfetch Benchmark</h1>
|
||||
<button id="theme-toggle" class="theme-toggle" type="button"></button>
|
||||
</div>
|
||||
<p class="intro">Performance history grouped by CPU family. Each chart contains all modules and total process time for one operating system and architecture. Module curves are available from the legend and hidden by default.</p>
|
||||
<p class="intro">Performance history grouped by CPU family. Each chart contains all modules and the total process time for one operating system and architecture. All metric series are shown by default except total process time, module Weather, and module PublicIp — toggle any series from the legend below the chart.</p>
|
||||
<p class="links"><a href="data.js">Download raw benchmark data</a></p>
|
||||
</header>
|
||||
<nav id="tabs" class="tabs" aria-label="CPU architecture"></nav>
|
||||
<main id="panels"></main>
|
||||
<main id="panels"><p class="empty">Loading charts…</p></main>
|
||||
<script>
|
||||
(() => {
|
||||
const benchmarkData = window.BENCHMARK_DATA || {};
|
||||
@@ -103,11 +104,12 @@
|
||||
}
|
||||
|
||||
function entryMetadata(entry) {
|
||||
const raw = entry.commit && entry.commit.timestamp || entry.date;
|
||||
const raw = entry.commit?.timestamp ?? entry.date;
|
||||
const date = new Date(raw);
|
||||
return {
|
||||
date: Number.isNaN(date.valueOf()) ? String(entry.date || '') : date.toISOString(),
|
||||
sha: entry.commit && entry.commit.id ? entry.commit.id : 'unknown'
|
||||
date: Number.isNaN(date.valueOf()) ? String(entry.date ?? '') : date.toISOString(),
|
||||
sha: entry.commit?.id ?? 'unknown',
|
||||
message: entry.commit?.message?.trim() ?? ''
|
||||
};
|
||||
}
|
||||
|
||||
@@ -131,6 +133,8 @@
|
||||
'#2563eb', '#dc2626', '#16a34a', '#9333ea', '#ea580c', '#0891b2',
|
||||
'#db2777', '#65a30d', '#4f46e5', '#ca8a04', '#0f766e', '#be123c'
|
||||
];
|
||||
// Series hidden on first render; every other metric is shown by default.
|
||||
const hiddenByDefault = new Set(['total process time', 'module Weather', 'module PublicIp']);
|
||||
const tabs = document.getElementById('tabs');
|
||||
const panels = document.getElementById('panels');
|
||||
const root = document.documentElement;
|
||||
@@ -139,6 +143,13 @@
|
||||
let theme = savedTheme || (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
||||
const chartsByInstance = [];
|
||||
|
||||
if (!window.Chart) {
|
||||
tabs.hidden = true;
|
||||
panels.innerHTML = '<p class="empty">Failed to load Chart.js.</p>';
|
||||
return;
|
||||
}
|
||||
panels.replaceChildren();
|
||||
|
||||
function chartColors() {
|
||||
const styles = getComputedStyle(root);
|
||||
return {
|
||||
@@ -167,6 +178,12 @@
|
||||
const row = chart._benchmarkRows[tooltip.dataPoints[0].dataIndex];
|
||||
title.textContent = `${new Date(row.date).toLocaleString()} · ${row.sha}`;
|
||||
tooltipElement.appendChild(title);
|
||||
if (row.message) {
|
||||
const message = document.createElement('div');
|
||||
message.className = 'chart-tooltip-message';
|
||||
message.textContent = row.message;
|
||||
tooltipElement.appendChild(message);
|
||||
}
|
||||
const bodyElement = document.createElement('div');
|
||||
bodyElement.className = 'chart-tooltip-body';
|
||||
for (const body of [...tooltip.dataPoints].sort((a, b) => b.parsed.y - a.parsed.y)) {
|
||||
@@ -229,7 +246,9 @@
|
||||
families.get(family).push({ key, rows });
|
||||
}
|
||||
|
||||
const familyOrder = ['Intel', 'ARM', 'Other'];
|
||||
// 'Other' is intentionally not rendered: it only catches stray series (e.g. the retired
|
||||
// 'macos universal' records) that should not show up as their own tab
|
||||
const familyOrder = ['Intel', 'ARM'];
|
||||
const availableFamilies = familyOrder.filter(family => families.has(family));
|
||||
|
||||
for (const [index, family] of availableFamilies.entries()) {
|
||||
@@ -311,7 +330,7 @@
|
||||
pointRadius: 3,
|
||||
tension: 0.2,
|
||||
spanGaps: true,
|
||||
hidden: metric !== 'total process time'
|
||||
hidden: hiddenByDefault.has(metric)
|
||||
}))
|
||||
},
|
||||
options: {
|
||||
@@ -373,7 +392,7 @@
|
||||
chart.update();
|
||||
});
|
||||
addLegendAction('Reset', () => {
|
||||
metrics.forEach((metric, metricIndex) => chart.setDatasetVisibility(metricIndex, metric === 'total process time'));
|
||||
metrics.forEach((metric, metricIndex) => chart.setDatasetVisibility(metricIndex, !hiddenByDefault.has(metric)));
|
||||
refreshLegend();
|
||||
chart.update();
|
||||
});
|
||||
@@ -395,7 +414,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (!groups.size) {
|
||||
if (!availableFamilies.length) {
|
||||
tabs.hidden = true;
|
||||
panels.innerHTML = '<p class="empty">No benchmark data available.</p>';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
name: Reusable Android
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
abi:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
CMAKE_BUILD_TYPE: ${{ vars.CMAKE_BUILD_TYPE || 'RelWithDebInfo' }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: uname -a
|
||||
run: uname -a
|
||||
|
||||
# The NDK comes with the runner image, so nothing is downloaded here. The image is pinned to
|
||||
# ubuntu-24.04 rather than ubuntu-latest because which NDK a build used is part of what it is.
|
||||
#
|
||||
# The image's *latest* NDK, not its default one: the default is r27d (clang 18), while fastfetch
|
||||
# is built and shipped on Termux with clang 21. Testing three majors behind the compiler the
|
||||
# supported path uses buys nothing and rejects code clang 21 accepts.
|
||||
- name: print NDK version
|
||||
run: |
|
||||
echo "default: ${ANDROID_NDK_HOME}"
|
||||
echo "latest: ${ANDROID_NDK_LATEST_HOME}"
|
||||
test -f "${ANDROID_NDK_LATEST_HOME}/build/cmake/android.toolchain.cmake"
|
||||
cat "${ANDROID_NDK_LATEST_HOME}/source.properties"
|
||||
"${ANDROID_NDK_LATEST_HOME}"/toolchains/llvm/prebuilt/*/bin/clang --version
|
||||
|
||||
# freetype is the one library this build wants that the NDK does not ship: its headers are not
|
||||
# in the sysroot and the runner image has no libfreetype-dev, so without this step
|
||||
# ff_lib_enable(FREETYPE ...) prints "Library: missing: FREETYPE" and
|
||||
# src/detection/terminalfont/terminalfont_android.c compiles only its #else branch. That is the
|
||||
# branch the shipped Termux build never takes, so the freetype code would rot untested here.
|
||||
#
|
||||
# Handing it the host's copy is sound, for two reasons. fastfetch never links freetype --
|
||||
# BINARY_LINK_TYPE is dlopen, so ff_lib_enable skips its target_link_libraries and only the
|
||||
# include path and FF_HAVE_FREETYPE are needed. And freetype's public headers carry no host
|
||||
# assumptions: FT_SIZEOF_LONG and FT_SIZEOF_INT in freetype/config/integer-types.h are derived
|
||||
# from FT_ULONG_MAX/FT_UINT_MAX, which come from the *target's* <limits.h>, so the same copy
|
||||
# describes FT_FaceRec -- and the offset of the family_name that terminalfont_android.c reads --
|
||||
# correctly for aarch64, armv7 and x86_64 alike.
|
||||
#
|
||||
# pkg-config is the only route that can find it: the NDK toolchain leaves PKG_CONFIG_PATH,
|
||||
# PKG_CONFIG_LIBDIR and PKG_CONFIG_SYSROOT_DIR untouched, so the ordinary host search path is
|
||||
# what gets used.
|
||||
#
|
||||
# The .pc is written here rather than taken from libfreetype-dev, whose own copy carries
|
||||
# `Requires.private: zlib, bzip2, libpng, harfbuzz, libbrotlidec`. pkgconf -- which is what
|
||||
# `pkg-config` has been on Ubuntu since 22.10 -- fails outright on a private requirement it
|
||||
# cannot resolve, even for --exists, and libfreetype-dev only pulls in libpng-dev and
|
||||
# zlib1g-dev. Nothing is linked, so the include directory is the whole contract. A Version
|
||||
# field is mandatory (pkgconf treats a .pc without one as not found), hence the sed.
|
||||
- name: install freetype headers
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install -y --no-install-recommends libfreetype-dev
|
||||
test -f /usr/include/freetype2/ft2build.h
|
||||
VERSION=$(awk '/^#define FREETYPE_(MAJOR|MINOR|PATCH)/ {printf "%s%s", sep, $3; sep="."}' /usr/include/freetype2/freetype/freetype.h)
|
||||
test -n "${VERSION}"
|
||||
mkdir -p freetype-pc
|
||||
cat > freetype-pc/freetype2.pc <<'EOF'
|
||||
prefix=/usr
|
||||
includedir=${prefix}/include
|
||||
|
||||
Name: FreeType 2
|
||||
Description: A free, high-quality, and portable font engine.
|
||||
Version: 0
|
||||
Cflags: -I${includedir}/freetype2
|
||||
EOF
|
||||
sed -i "s/^Version: .*/Version: ${VERSION}/" freetype-pc/freetype2.pc
|
||||
PKG_CONFIG_PATH="$PWD/freetype-pc" pkg-config --cflags freetype2
|
||||
|
||||
# android-24 (Android 7.0) is Termux's own floor, and it is the floor for a second, independent
|
||||
# reason: libcamera2ndk.so only exists from API 24, fastfetch links it unconditionally, and the
|
||||
# NDK has no such thing as a weak *library* reference -- a build aimed lower would fail to load
|
||||
# on exactly the devices it was aimed at. The level is also what makes this job check anything:
|
||||
# at 24 the NDK marks AImageDecoder_* / AndroidBitmap_compress (30) and AMediaCodec_getName /
|
||||
# AMediaCodec_releaseName (28) unavailable, which is what CMakeLists.txt's
|
||||
# -D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability pair is built on.
|
||||
# Building against a newer platform would quietly stop enforcing it.
|
||||
#
|
||||
# BUILD_TESTS stays off. The test binaries are host programs, there is no device here to run
|
||||
# them on, and `ctest` therefore cannot follow -- unlike every other job in this workflow set.
|
||||
# The platform branch of the build is what this job covers.
|
||||
- name: configure project
|
||||
env:
|
||||
# Picks up the freetype2.pc written by the step above. The rest of the search path is left
|
||||
# alone, so zlib and every other host .pc file is still found exactly as before.
|
||||
PKG_CONFIG_PATH: ${{ github.workspace }}/freetype-pc
|
||||
run: cmake -DSET_TWEAK=Off -DBUILD_TESTS=Off -DCMAKE_TOOLCHAIN_FILE="${ANDROID_NDK_LATEST_HOME}/build/cmake/android.toolchain.cmake" -DANDROID_ABI=${{ inputs.abi }} -DANDROID_PLATFORM=android-24 -DCMAKE_INSTALL_PREFIX=/data/data/com.termux/files/usr .
|
||||
|
||||
# A silent regression back to "Library: missing: FREETYPE" would look exactly like a passing
|
||||
# build -- the module would simply stop being compiled, and the only trace would be a line in
|
||||
# the configure log nobody diffs. Assert that the define actually reached the compile line.
|
||||
- name: check freetype was detected
|
||||
run: |
|
||||
grep -q -- '-DFF_HAVE_FREETYPE=1' CMakeFiles/libfastfetch.dir/flags.make || {
|
||||
echo "::error::freetype was not detected, so terminalfont_android.c compiled its #else branch only"
|
||||
grep -o -- '-DFF_HAVE_[A-Z0-9_]*' CMakeFiles/libfastfetch.dir/flags.make | sort -u
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: build project
|
||||
run: cmake --build . --target package --verbose -j4
|
||||
|
||||
- name: print ELF header
|
||||
run: readelf --wide -h fastfetch
|
||||
|
||||
- name: print dynamic dependencies
|
||||
run: readelf --wide -d fastfetch
|
||||
|
||||
# libjnigraphics.so, libmediandk.so and libcamera2ndk.so are what provide the entry points
|
||||
# CMakeLists.txt weakens. A weak reference resolves to null instead of failing the load, and
|
||||
# FF_ANDROID_API_AT_LEAST() reads the platform level rather than the symbol -- so a library
|
||||
# dropped from the link line still passes the guard on a device new enough, and the call goes
|
||||
# through a null pointer. DT_NEEDED is the only place that mistake is visible.
|
||||
- name: check linked libraries
|
||||
run: |
|
||||
for lib in libjnigraphics.so libmediandk.so libcamera2ndk.so; do
|
||||
readelf --wide -d fastfetch | grep -q "NEEDED.*\[${lib}\]" || { echo "::error::${lib} is not in DT_NEEDED"; exit 1; }
|
||||
done
|
||||
|
||||
# Every reference to an entry point the NDK marks unavailable at API 24 has to be weak: a strong
|
||||
# one turns a device below API 28/30 into a load failure instead of a graceful downgrade, which
|
||||
# is the entire reason those three files are compiled with -D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__.
|
||||
# `nm -D` prints a weak undefined symbol as `w` and a strong undefined one as `U`.
|
||||
#
|
||||
# The families are whole prefixes on purpose: every AImageDecoder* entry point is API 30 or 31,
|
||||
# but the frame-info and header-info ones spell the prefix AImageDecoderFrameInfo / HeaderInfo
|
||||
# without an underscore, and matching only `AImageDecoder_` would leave them unchecked. The
|
||||
# AMediaCodec ones are named one by one because the rest of that API -- createDecoderByType,
|
||||
# createEncoderByType, delete -- is API 21 and is correctly a strong reference.
|
||||
- name: check weak references
|
||||
run: |
|
||||
nm -D fastfetch | grep -E ' (AImageDecoder|AndroidBitmap_compress|AMediaCodec_getName$|AMediaCodec_releaseName$)' > weak.txt
|
||||
cat weak.txt
|
||||
test -s weak.txt || { echo "::error::none of these entry points is referenced any more -- if the last use is gone on purpose, this check and the -Werror=unguarded-availability pair it guards should go with it"; exit 1; }
|
||||
if grep -qv '^ *w ' weak.txt; then
|
||||
echo "::error::expected every one of these to be a weak reference:"
|
||||
grep -v '^ *w ' weak.txt
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: print size
|
||||
run: |
|
||||
file fastfetch
|
||||
ls -l fastfetch
|
||||
|
||||
- name: upload artifacts
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: fastfetch-android-${{ inputs.abi }}
|
||||
path: ./fastfetch-*.*
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
run: cp .github/benchmark-index.html "$RUNNER_TEMP/benchmark-index.html"
|
||||
|
||||
- name: store benchmark result
|
||||
uses: benchmark-action/github-action-benchmark@52576c92bccf6ac60c8223ec7eb2565637cae9ba # v1.22.1
|
||||
uses: benchmark-action/github-action-benchmark@4322e5726e6334590d251fc4f92bec0efafc45dc # v1.22.2
|
||||
with:
|
||||
name: Fastfetch
|
||||
tool: customSmallerIsBetter
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
sudo pkg install -y llvm cmake git pkgconf binutils wayland vulkan-headers vulkan-loader libxcb libXrandr libX11 libdrm glib dconf dbus sqlite3-tcl egl opencl ocl-icd v4l_compat chafa libelf lua54 libva libvdpau
|
||||
|
||||
- name: CMake configuration
|
||||
run: env CC=clang cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_BUILD_TYPE=${{ vars.CMAKE_BUILD_TYPE || 'RelWithDebInfo' }} .
|
||||
run: env CC=clang cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_BUILD_TYPE=${{ vars.CMAKE_BUILD_TYPE || 'RelWithDebInfo' }} -DCMAKE_C_FLAGS="-fno-pie" -DCMAKE_EXE_LINKER_FLAGS="-no-pie" .
|
||||
|
||||
- name: CMake build
|
||||
run: cmake --build . --target package --verbose -j4
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
sudo pkg install -y cmake git pkgconf binutils wayland vulkan-headers vulkan-loader libxcb libXrandr libX11 libdrm glib dconf dbus sqlite3 egl opencl ocl-icd v4l_compat chafa lua54 libva libvdpau
|
||||
|
||||
- name: CMake configuration
|
||||
run: cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_BUILD_TYPE=${{ vars.CMAKE_BUILD_TYPE || 'RelWithDebInfo' }} .
|
||||
run: cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_BUILD_TYPE=${{ vars.CMAKE_BUILD_TYPE || 'RelWithDebInfo' }} -DCMAKE_C_FLAGS="-fno-pie" -DCMAKE_EXE_LINKER_FLAGS="-no-pie" .
|
||||
|
||||
- name: CMake build
|
||||
run: cmake --build . --target package --verbose -j4
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: run VM
|
||||
uses: uraimo/run-on-arch-action@460cb8e6d9f726a588fc9b5e681c8a6cab09ae41 # v3.2.0
|
||||
uses: uraimo/run-on-arch-action@fa1f3e7de95534497266c7265950b5188355fbfb # v3.2.1
|
||||
id: runcmd
|
||||
with:
|
||||
arch: armv7
|
||||
@@ -30,8 +30,8 @@ jobs:
|
||||
curl -L https://apt.kitware.com/keys/kitware-archive-latest.asc | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null
|
||||
echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ jammy main' | tee /etc/apt/sources.list.d/kitware.list >/dev/null
|
||||
echo -e 'Acquire::https::Verify-Peer "false";\nAcquire::https::Verify-Host "false";' >> /etc/apt/apt.conf.d/99ignore-certificates
|
||||
apt-get update && apt-get install -y cmake make gcc-13 libvulkan-dev libwayland-dev libxrandr-dev libxcb-randr0-dev libdconf-dev libdbus-1-dev libmagickcore-dev libsqlite3-dev librpm-dev libegl-dev libglx-dev ocl-icd-opencl-dev libpulse-dev libdrm-dev libelf-dev libefl-all-dev liblua5.4-dev libvdpau-dev libva-dev rpm
|
||||
CC=gcc-13 cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DCMAKE_INSTALL_PREFIX=/usr .
|
||||
apt-get update && apt-get install -y cmake make gcc-13 libvulkan-dev libwayland-dev libxrandr-dev libxcb-randr0-dev libdconf-dev libdbus-1-dev libmagickcore-dev libsqlite3-dev librpm-dev libegl-dev libglx-dev ocl-icd-opencl-dev libdrm-dev libelf-dev libefl-all-dev liblua5.4-dev libvdpau-dev libva-dev rpm
|
||||
CC=gcc-13 cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_C_FLAGS="-fno-pie" -DCMAKE_EXE_LINKER_FLAGS="-no-pie" .
|
||||
cmake --build . --target package --verbose -j4
|
||||
./fastfetch --list-features
|
||||
time ./fastfetch -c presets/ci.jsonc --stat false
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
run: sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test
|
||||
|
||||
- name: install required packages
|
||||
run: sudo apt-get update && sudo apt-get install -y gcc-13 jq libvulkan-dev libwayland-dev libxrandr-dev libxcb-randr0-dev libdconf-dev libdbus-1-dev libmagickcore-dev libsqlite3-dev librpm-dev libegl-dev libglx-dev ocl-icd-opencl-dev libpulse-dev libdrm-dev libelf-dev libddcutil-dev libefl-all-dev libunwind-dev liblua5.4-dev libvdpau-dev libva-dev rpm ninja-build
|
||||
run: sudo apt-get update && sudo apt-get install -y gcc-13 jq libvulkan-dev libwayland-dev libxrandr-dev libxcb-randr0-dev libdconf-dev libdbus-1-dev libmagickcore-dev libsqlite3-dev librpm-dev libegl-dev libglx-dev ocl-icd-opencl-dev libdrm-dev libelf-dev libefl-all-dev libunwind-dev liblua5.4-dev libvdpau-dev libva-dev rpm ninja-build
|
||||
|
||||
- name: install linuxbrew packages
|
||||
run: |
|
||||
@@ -52,19 +52,19 @@ jobs:
|
||||
|
||||
- name: Initialize CodeQL
|
||||
if: inputs.arch == 'amd64'
|
||||
uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
||||
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||
with:
|
||||
languages: c
|
||||
|
||||
- name: configure project
|
||||
run: CC=gcc-13 CMAKE_PREFIX_PATH=/home/linuxbrew/.linuxbrew PKG_CONFIG_PATH=/home/linuxbrew/.linuxbrew/lib/pkgconfig:$PKG_CONFIG_PATH cmake -GNinja -DSET_TWEAK=Off -DBUILD_TESTS=On -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_INSTALL_PREFIX=/usr .
|
||||
run: CC=gcc-13 CMAKE_PREFIX_PATH=/home/linuxbrew/.linuxbrew PKG_CONFIG_PATH=/home/linuxbrew/.linuxbrew/lib/pkgconfig:$PKG_CONFIG_PATH cmake -GNinja -DSET_TWEAK=Off -DBUILD_TESTS=On -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_C_FLAGS="-fno-pie" -DCMAKE_EXE_LINKER_FLAGS="-no-pie" .
|
||||
|
||||
- name: build project
|
||||
run: cmake --build . --target package --verbose -j4
|
||||
|
||||
- name: perform CodeQL analysis
|
||||
if: inputs.arch == 'amd64'
|
||||
uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
||||
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||
|
||||
- name: list features
|
||||
run: ./fastfetch --list-features
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
run: sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test
|
||||
|
||||
- name: install required packages
|
||||
run: sudo apt-get update && sudo apt-get install -y gcc-13 gcc-13-multilib libvulkan-dev libwayland-dev libxrandr-dev libxcb-randr0-dev libdconf-dev libdbus-1-dev libmagickcore-dev libsqlite3-dev librpm-dev libegl-dev libglx-dev ocl-icd-opencl-dev libpulse-dev libdrm-dev libelf-dev libddcutil-dev libefl-all-dev libunwind-dev liblua5.4-dev libvdpau-dev libva-dev rpm ninja-build
|
||||
run: sudo apt-get update && sudo apt-get install -y gcc-13 gcc-13-multilib libvulkan-dev libwayland-dev libxrandr-dev libxcb-randr0-dev libdconf-dev libdbus-1-dev libmagickcore-dev libsqlite3-dev librpm-dev libegl-dev libglx-dev ocl-icd-opencl-dev libdrm-dev libelf-dev libefl-all-dev libunwind-dev liblua5.4-dev libvdpau-dev libva-dev rpm ninja-build
|
||||
|
||||
- name: install linuxbrew packages
|
||||
run: |
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
run: cmake --version
|
||||
|
||||
- name: configure project
|
||||
run: CC=gcc-13 CMAKE_PREFIX_PATH=/home/linuxbrew/.linuxbrew PKG_CONFIG_PATH=/home/linuxbrew/.linuxbrew/lib/pkgconfig:$PKG_CONFIG_PATH cmake -DCMAKE_C_FLAGS="-m32 -march=i686 -mtune=i686" -DCMAKE_SYSTEM_NAME=Linux -DCMAKE_SYSTEM_VERSION=5.10.0 -DCMAKE_SYSTEM_PROCESSOR=i686 -DCPACK_DEBIAN_PACKAGE_ARCHITECTURE=i386 -GNinja -DSET_TWEAK=Off -DBUILD_TESTS=On -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_INSTALL_PREFIX=/usr .
|
||||
run: CC=gcc-13 CMAKE_PREFIX_PATH=/home/linuxbrew/.linuxbrew PKG_CONFIG_PATH=/home/linuxbrew/.linuxbrew/lib/pkgconfig:$PKG_CONFIG_PATH cmake -DCMAKE_C_FLAGS="-m32 -march=i686 -mtune=i686 -fno-pie" -DCMAKE_SYSTEM_NAME=Linux -DCMAKE_SYSTEM_VERSION=5.10.0 -DCMAKE_SYSTEM_PROCESSOR=i686 -DCPACK_DEBIAN_PACKAGE_ARCHITECTURE=i386 -GNinja -DSET_TWEAK=Off -DBUILD_TESTS=On -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_EXE_LINKER_FLAGS="-no-pie" .
|
||||
|
||||
- name: build project
|
||||
run: cmake --build . --target package --verbose -j4
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: set up QEMU for loong64
|
||||
uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0
|
||||
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
|
||||
|
||||
- name: build in loong64 container
|
||||
run: |
|
||||
@@ -37,13 +37,13 @@ jobs:
|
||||
libdconf-dev libdbus-1-dev libmagickcore-dev \
|
||||
libsqlite3-dev librpm-dev \
|
||||
libegl-dev libglx-dev ocl-icd-opencl-dev \
|
||||
libpulse-dev libdrm-dev \
|
||||
libdrm-dev \
|
||||
libelf-dev libefl-all-dev \
|
||||
liblua5.4-dev \
|
||||
libvdpau-dev libva-dev \
|
||||
rpm
|
||||
|
||||
cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DCMAKE_INSTALL_PREFIX=/usr .
|
||||
cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_C_FLAGS="-fno-pie" -DCMAKE_EXE_LINKER_FLAGS="-no-pie" .
|
||||
cmake --build . --target package --verbose -j$(nproc)
|
||||
./fastfetch --list-features
|
||||
time ./fastfetch -c presets/ci.jsonc --stat false
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: run VM
|
||||
uses: uraimo/run-on-arch-action@460cb8e6d9f726a588fc9b5e681c8a6cab09ae41 # v3.2.0
|
||||
uses: uraimo/run-on-arch-action@fa1f3e7de95534497266c7265950b5188355fbfb # v3.2.1
|
||||
id: runcmd
|
||||
with:
|
||||
arch: ${{ inputs.arch }}
|
||||
@@ -31,8 +31,8 @@ jobs:
|
||||
uname -a
|
||||
apt-get update && apt-get install -y software-properties-common
|
||||
add-apt-repository -y ppa:ubuntu-toolchain-r/test
|
||||
apt-get update && apt-get install -y cmake make gcc-13 libvulkan-dev libwayland-dev libxrandr-dev libxcb-randr0-dev libdconf-dev libdbus-1-dev libmagickcore-dev libsqlite3-dev librpm-dev libegl-dev libglx-dev ocl-icd-opencl-dev libpulse-dev libdrm-dev libchafa-dev libelf-dev libefl-all-dev liblua5.4-dev libvdpau-dev libva-dev rpm
|
||||
CC=gcc-13 cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DCMAKE_INSTALL_PREFIX=/usr .
|
||||
apt-get update && apt-get install -y cmake make gcc-13 libvulkan-dev libwayland-dev libxrandr-dev libxcb-randr0-dev libdconf-dev libdbus-1-dev libmagickcore-dev libsqlite3-dev librpm-dev libegl-dev libglx-dev ocl-icd-opencl-dev libdrm-dev libchafa-dev libelf-dev libefl-all-dev liblua5.4-dev libvdpau-dev libva-dev rpm
|
||||
CC=gcc-13 cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_C_FLAGS="-fno-pie" -DCMAKE_EXE_LINKER_FLAGS="-no-pie" .
|
||||
cmake --build . --target package --verbose -j4
|
||||
./fastfetch --list-features
|
||||
time ./fastfetch -c presets/ci.jsonc --stat false
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
name: Reusable macOS Hosts
|
||||
name: Reusable macOS Universal
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
arch:
|
||||
required: true
|
||||
type: string
|
||||
runs-on:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -18,7 +11,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ inputs.runs-on }}
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -26,34 +19,20 @@ jobs:
|
||||
- name: uname -a
|
||||
run: uname -a
|
||||
|
||||
- name: install required packages with MacPorts # Homebrew dropped Intel support
|
||||
if: inputs.arch == 'amd64'
|
||||
run: |
|
||||
curl -fsSL https://github.com/macports/macports-base/releases/download/v2.12.6/MacPorts-2.12.6-15-Sequoia.pkg -o "$RUNNER_TEMP/MacPorts.pkg"
|
||||
sudo installer -pkg "$RUNNER_TEMP/MacPorts.pkg" -target /
|
||||
echo /opt/local/bin >> "$GITHUB_PATH"
|
||||
echo /opt/local/sbin >> "$GITHUB_PATH"
|
||||
export PATH=/opt/local/bin:/opt/local/sbin:$PATH
|
||||
sudo port selfupdate
|
||||
sudo port install vulkan-loader vulkan-headers MoltenVK ImageMagick7 chafa lua quickjs-ng jq
|
||||
|
||||
- name: install required packages with Homebrew
|
||||
if: inputs.arch == 'aarch64'
|
||||
run: |
|
||||
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 brew update
|
||||
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 brew install --overwrite vulkan-loader vulkan-headers molten-vk imagemagick chafa lua quickjs-ng jq
|
||||
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 brew install --overwrite vulkan-loader vulkan-headers molten-vk chafa lua jq
|
||||
|
||||
- name: configure project
|
||||
run: |
|
||||
if [ "${{ inputs.arch }}" = "amd64" ]; then
|
||||
export CMAKE_PREFIX_PATH=/opt/local
|
||||
export PKG_CONFIG_PATH=/opt/local/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}
|
||||
fi
|
||||
cmake -DSET_TWEAK=Off -DBUILD_TESTS=On .
|
||||
run: cmake -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" -DSET_TWEAK=Off -DBUILD_TESTS=On .
|
||||
|
||||
- name: build project
|
||||
run: cmake --build . --target package --verbose -j4
|
||||
|
||||
- name: verify universal binary
|
||||
run: lipo -archs fastfetch
|
||||
|
||||
- name: list features
|
||||
run: ./fastfetch --list-features
|
||||
|
||||
@@ -72,7 +51,7 @@ jobs:
|
||||
run: |
|
||||
real_seconds=$(awk '$1 == "real" { print $2 }' fastfetch.time)
|
||||
jq -e 'type == "array"' fastfetch.json > /dev/null
|
||||
jq --arg platform "macos" --arg arch "${{ inputs.arch }}" --argjson real_seconds "$real_seconds" '
|
||||
jq --arg platform "macos" --arg arch "aarch64" --argjson real_seconds "$real_seconds" '
|
||||
map(select(.stat | type == "number") | {
|
||||
name: ("fastfetch " + $platform + " " + $arch + " module " + .type),
|
||||
unit: "ms",
|
||||
@@ -97,11 +76,11 @@ jobs:
|
||||
- name: upload artifacts
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: fastfetch-macos-${{ inputs.arch }}
|
||||
name: fastfetch-macos-universal
|
||||
path: ./fastfetch-*.*
|
||||
|
||||
- name: upload benchmark data
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: benchmark-macos-${{ inputs.arch }}
|
||||
name: benchmark-macos-universal
|
||||
path: benchmark.json
|
||||
|
||||
@@ -24,12 +24,12 @@ jobs:
|
||||
run: |
|
||||
cat /etc/alpine-release
|
||||
uname -a
|
||||
apk add cmake samurai vulkan-loader-dev libxcb-dev libxrandr-dev rpm-dev wayland-dev libdrm-dev dconf-dev imagemagick-dev chafa-dev zlib-dev dbus-dev mesa-dev opencl-dev sqlite-dev networkmanager-dev pulseaudio-dev ddcutil-dev elfutils-dev lua5.4-dev quickjs-ng-dev libvdpau-dev libva-dev gcc g++
|
||||
apk add cmake samurai vulkan-loader-dev libxcb-dev libxrandr-dev rpm-dev wayland-dev libdrm-dev dconf-dev imagemagick-dev chafa-dev zlib-dev dbus-dev mesa-dev opencl-dev sqlite-dev networkmanager-dev elfutils-dev lua5.4-dev quickjs-ng-dev libvdpau-dev libva-dev gcc g++
|
||||
shell: alpine.sh --root {0}
|
||||
|
||||
- name: build
|
||||
run: |
|
||||
cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DCMAKE_INSTALL_PREFIX=/usr -DIS_MUSL=ON -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -GNinja .
|
||||
cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DCMAKE_INSTALL_PREFIX=/usr -DIS_MUSL=ON -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_C_FLAGS="-fno-pie" -DCMAKE_EXE_LINKER_FLAGS="-no-pie" -GNinja .
|
||||
cmake --build . --target package --verbose -j4
|
||||
shell: alpine.sh {0}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
sudo pkgin -y install clang cmake git pkgconf wayland vulkan-headers dconf dbus sqlite3 ImageMagick libva libvdpau
|
||||
|
||||
- name: CMake configuration
|
||||
run: CC=clang cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_BUILD_TYPE=${{ vars.CMAKE_BUILD_TYPE || 'RelWithDebInfo' }} .
|
||||
run: CC=clang cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_BUILD_TYPE=${{ vars.CMAKE_BUILD_TYPE || 'RelWithDebInfo' }} -DCMAKE_C_FLAGS="-fno-pie" -DCMAKE_EXE_LINKER_FLAGS="-no-pie" .
|
||||
|
||||
- name: CMake build
|
||||
run: cmake --build . --target package --verbose -j4
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
run: uname -a
|
||||
|
||||
- name: configure project
|
||||
run: cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DCMAKE_INSTALL_PREFIX=/usr . -DENABLE_VULKAN=OFF -DENABLE_WAYLAND=OFF -DENABLE_XCB_RANDR=OFF -DENABLE_XCB=OFF -DENABLE_XRANDR=OFF -DENABLE_X11=OFF -DENABLE_DRM=OFF -DENABLE_DRM_AMDGPU=OFF -DENABLE_GIO=OFF -DENABLE_DCONF=OFF -DENABLE_DBUS=OFF -DENABLE_SQLITE3=OFF -DENABLE_RPM=OFF -DENABLE_IMAGEMAGICK7=OFF -DENABLE_IMAGEMAGICK6=OFF -DENABLE_CHAFA=OFF -DENABLE_ZLIB=OFF -DENABLE_EGL=OFF -DENABLE_GLX=OFF -DENABLE_OPENCL=OFF -DENABLE_FREETYPE=OFF -DENABLE_PULSE=OFF -DENABLE_DDCUTIL=OFF -DENABLE_ELF=OFF -DENABLE_EET=OFF -DENABLE_THREADS=OFF -DENABLE_IMAGE_LOGO=OFF
|
||||
run: cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_C_FLAGS="-fno-pie" -DCMAKE_EXE_LINKER_FLAGS="-no-pie" . -DENABLE_VULKAN=OFF -DENABLE_WAYLAND=OFF -DENABLE_XCB_RANDR=OFF -DENABLE_XCB=OFF -DENABLE_XRANDR=OFF -DENABLE_X11=OFF -DENABLE_DRM=OFF -DENABLE_DRM_AMDGPU=OFF -DENABLE_GIO=OFF -DENABLE_DCONF=OFF -DENABLE_DBUS=OFF -DENABLE_SQLITE3=OFF -DENABLE_RPM=OFF -DENABLE_IMAGEMAGICK7=OFF -DENABLE_IMAGEMAGICK6=OFF -DENABLE_CHAFA=OFF -DENABLE_ZLIB=OFF -DENABLE_EGL=OFF -DENABLE_GLX=OFF -DENABLE_OPENCL=OFF -DENABLE_FREETYPE=OFF -DENABLE_ELF=OFF -DENABLE_EET=OFF -DENABLE_THREADS=OFF -DENABLE_IMAGE_LOGO=OFF
|
||||
|
||||
- name: build project
|
||||
run: cmake --build . --target package --verbose -j4
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
sudo pkg install gcc14 cmake git pkg-config glib2 dbus sqlite-3 imagemagick ninja
|
||||
|
||||
- name: CMake configuration
|
||||
run: cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -GNinja -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_BUILD_TYPE=${{ vars.CMAKE_BUILD_TYPE || 'RelWithDebInfo' }} .
|
||||
run: cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -GNinja -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_BUILD_TYPE=${{ vars.CMAKE_BUILD_TYPE || 'RelWithDebInfo' }} -DCMAKE_C_FLAGS="-fno-pie" -DCMAKE_EXE_LINKER_FLAGS="-no-pie" .
|
||||
|
||||
- name: CMake build
|
||||
run: cmake --build . --verbose -j4
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
sudo pkg_add -r llvm-21.1.8p4 cmake git wayland vulkan-headers vulkan-loader glib2 dconf dbus sqlite3 imagemagick chafa lua-5.4.8p0 # pkg-config is preinstalled
|
||||
|
||||
- name: CMake configuration
|
||||
run: CC=clang-21 cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_BUILD_TYPE=${{ vars.CMAKE_BUILD_TYPE || 'RelWithDebInfo' }} .
|
||||
run: CC=clang-21 cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_BUILD_TYPE=${{ vars.CMAKE_BUILD_TYPE || 'RelWithDebInfo' }} -DCMAKE_C_FLAGS="-fno-pie" -DCMAKE_EXE_LINKER_FLAGS="-no-pie" .
|
||||
|
||||
- name: CMake build
|
||||
run: cmake --build . --target package --verbose -j4
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: run VM
|
||||
uses: vmactions/solaris-vm@a11f407d3720a3717673d4194f19ee63b284977e # v1.4.0
|
||||
uses: vmactions/solaris-vm@87d3c436511cef802cd1637f86f73b1a97715c8c # v1.4.1
|
||||
with:
|
||||
usesh: true
|
||||
envs: 'CMAKE_BUILD_TYPE'
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
|
||||
run: |
|
||||
export PKG_CONFIG_PATH=/usr/lib/64/pkgconfig
|
||||
cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -GNinja .
|
||||
cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -GNinja -DCMAKE_C_FLAGS="-fno-pie" -DCMAKE_EXE_LINKER_FLAGS="-no-pie" .
|
||||
cmake --build . --verbose -j4
|
||||
./fastfetch --list-features
|
||||
time ./fastfetch -c presets/ci.jsonc --stat false
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
name: Reusable Spellcheck
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
spellcheck:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Install codespell
|
||||
shell: bash
|
||||
run: |
|
||||
pip3 install codespell
|
||||
codespell --version
|
||||
|
||||
- name: Run Spellchecker
|
||||
run: codespell
|
||||
@@ -45,11 +45,11 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: setup-msys2
|
||||
uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2.32.0
|
||||
uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2.33.0
|
||||
with:
|
||||
msystem: ${{ inputs.msystem }}
|
||||
update: true
|
||||
install: git mingw-w64-clang-${{ inputs.msys-arch }}-7zip mingw-w64-clang-${{ inputs.msys-arch }}-cmake mingw-w64-clang-${{ inputs.msys-arch }}-clang mingw-w64-clang-${{ inputs.msys-arch }}-jq mingw-w64-clang-${{ inputs.msys-arch }}-vulkan-loader mingw-w64-clang-${{ inputs.msys-arch }}-vulkan-headers mingw-w64-clang-${{ inputs.msys-arch }}-opencl-icd mingw-w64-clang-${{ inputs.msys-arch }}-opencl-headers mingw-w64-clang-${{ inputs.msys-arch }}-cppwinrt mingw-w64-clang-${{ inputs.msys-arch }}-imagemagick mingw-w64-clang-${{ inputs.msys-arch }}-chafa mingw-w64-clang-${{ inputs.msys-arch }}-lua mingw-w64-clang-${{ inputs.msys-arch }}-quickjs-ng mingw-w64-clang-${{ inputs.msys-arch }}-directx-headers
|
||||
install: git mingw-w64-clang-${{ inputs.msys-arch }}-7zip mingw-w64-clang-${{ inputs.msys-arch }}-cmake mingw-w64-clang-${{ inputs.msys-arch }}-clang mingw-w64-clang-${{ inputs.msys-arch }}-jq mingw-w64-clang-${{ inputs.msys-arch }}-vulkan-loader mingw-w64-clang-${{ inputs.msys-arch }}-vulkan-headers mingw-w64-clang-${{ inputs.msys-arch }}-opencl-icd mingw-w64-clang-${{ inputs.msys-arch }}-opencl-headers mingw-w64-clang-${{ inputs.msys-arch }}-cppwinrt mingw-w64-clang-${{ inputs.msys-arch }}-chafa mingw-w64-clang-${{ inputs.msys-arch }}-lua mingw-w64-clang-${{ inputs.msys-arch }}-directx-headers
|
||||
|
||||
- name: print msys version
|
||||
run: uname -a
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
run: cmake --build . --verbose -j4
|
||||
|
||||
- name: copy necessary dlls
|
||||
run: cp /${{ inputs.msystem-lower }}/bin/{lua55,libqjs-0}.dll .
|
||||
run: cp /${{ inputs.msystem-lower }}/bin/lua55.dll .
|
||||
|
||||
- name: list features
|
||||
run: ./fastfetch --list-features
|
||||
@@ -116,7 +116,8 @@ jobs:
|
||||
|
||||
- if: github.event_name == 'push' && github.repository == 'fastfetch-cli/fastfetch'
|
||||
name: submit signing request
|
||||
uses: signpath/github-action-submit-signing-request@f6d04783b4569d051e0c80105fe66e82819d0092 # v3.0
|
||||
# https://github.com/SignPath/github-action-submit-signing-request/issues/18
|
||||
uses: signpath/github-action-submit-signing-request@c92b958760219087e01f8d67a1669ed57afe2627 # v2.3
|
||||
with:
|
||||
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
|
||||
organization-id: '${{ vars.SIGNPATH_ORG_ID }}'
|
||||
|
||||
+16
-16
@@ -5,13 +5,7 @@ on:
|
||||
- pull_request
|
||||
|
||||
jobs:
|
||||
spellcheck:
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/build-spellcheck.yml
|
||||
|
||||
no-features-test:
|
||||
needs: spellcheck
|
||||
name: No-features-test
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -78,22 +72,27 @@ jobs:
|
||||
contents: read
|
||||
uses: ./.github/workflows/build-musl-amd64.yml
|
||||
|
||||
macos-hosts:
|
||||
android:
|
||||
needs: no-features-test
|
||||
name: macOS-${{ matrix.arch }}
|
||||
name: Android-${{ matrix.abi }}
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
runs-on: macos-15-intel
|
||||
- arch: aarch64
|
||||
runs-on: macos-latest
|
||||
uses: ./.github/workflows/build-macos-hosts.yml
|
||||
abi:
|
||||
- arm64-v8a
|
||||
- armeabi-v7a
|
||||
- x86_64
|
||||
uses: ./.github/workflows/build-android.yml
|
||||
with:
|
||||
arch: ${{ matrix.arch }}
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
abi: ${{ matrix.abi }}
|
||||
|
||||
macos-hosts:
|
||||
needs: no-features-test
|
||||
name: macOS-universal
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/build-macos-hosts.yml
|
||||
|
||||
omnios-amd64:
|
||||
needs: no-features-test
|
||||
@@ -196,6 +195,7 @@ jobs:
|
||||
- linux-loong64
|
||||
- linux-vms
|
||||
- musl-amd64
|
||||
- android
|
||||
- macos-hosts
|
||||
- freebsd-amd64
|
||||
- openbsd-amd64
|
||||
|
||||
@@ -16,3 +16,4 @@ fastfetch.kdev4
|
||||
/*.zh.md
|
||||
/.work*
|
||||
/doc/*.md
|
||||
/fastfetch.wiki
|
||||
|
||||
+102
-82
@@ -1,92 +1,112 @@
|
||||
# 2.70.0
|
||||
|
||||
Changes:
|
||||
* LocalIP flags on Windows now describe the interface itself — its operational status, its media connection state and its type, for example `ETHERNET_CSMACD`, `IEEE80211` or `SOFTWARE_LOOPBACK` — instead of the `IP_ADAPTER_*` bits.
|
||||
* Interfaces that carry no unicast address are no longer reported, since both the addresses and the interface properties are now enumerated from the unicast address table. (LocalIP, Windows)
|
||||
* libpulse and libddcutil are no longer used on Linux. Package managers should remove them from their fastfetch package.
|
||||
* QuickJS format scripting has been removed (deprecated in v2.69.0). Migrate scripts to Lua if necessary. (General)
|
||||
* The Command module option `useStdErr` has been replaced with `useOutput`, which selects which of the command's output streams to capture: `"stdout"`, `"stderr"` or `"both"`.
|
||||
* It defaults to `"both"`, so a command that logs to stderr now has that output captured instead of discarded.
|
||||
* Set it to `"stdout"` or `"stderr"` to restore the old single-stream behavior. (Command)
|
||||
* The Shell module now reports the shell from the password database (`/etc/passwd`) instead of the `$SHELL` environment variable, which may differ when the login shell was changed after the session started. (Title)
|
||||
* The Disk custom format no longer provides `{size-available}`. `{size-free}` now reports whichever of the two the `disk.useAvailable` option selects, and the raw byte counts are available as `{bytes-used}` and `{bytes-total}`. (Disk)
|
||||
* This also means `{size-free}` now changes with `disk.useAvailable`; it used to always report the file system's free size.
|
||||
* The Disk JSON output no longer includes `bytes.available` and `bytes.used`. (Disk)
|
||||
* `bytes.free` and `bytes.total` are unchanged; the used size can be derived as `total - free`.
|
||||
|
||||
Features:
|
||||
* Added Sound detection on Android. (Sound, Android)
|
||||
* Added display detection on Android over binder, which needs neither a child process nor a permission. (Display, Android)
|
||||
* Added battery temperature, remaining time and charger type (AC / USB / wireless) detection on Android. (Battery, Android)
|
||||
* Added media detection on Android when fastfetch runs as an app UID. (Media, Android)
|
||||
* Requires `Notification access` permission
|
||||
* Added app display-name detection on Android, so the media module names the player the way the app names itself instead of by its package name, and the terminal module reports the label Termux is installed under. (Media, Terminal, Android)
|
||||
* Performance improvements & internal cleanups
|
||||
* Added Finnish language support. (General)
|
||||
* Added pkgsrc package detection on macOS. (#2627, Packages, Apple)
|
||||
* Added an ALSA fallback to Sound on Linux, so a machine that runs no sound server reports its output device and volume instead of an error. (Sound, Linux)
|
||||
|
||||
Bugfixes:
|
||||
* Fixed `--config`/`-c` bash completion offering one candidate per word instead of one candidate per file for paths containing spaces. (Completion, Bash)
|
||||
* Fixed external monitors detected via DDC/CI being randomly reported as built-in on Linux. (#2615, Brightness, Linux)
|
||||
* Fixed the Vulkan driver name being concatenated with the name of a previously enumerated device on multi-GPU systems. (#2619, Vulkan)
|
||||
* Fixed the foot terminal font size not being parsed when written as `Font Name-size`, e.g. `Berkeley Mono-12`. (#2624, TerminalFont, Linux)
|
||||
* Fixed `/boot` being hidden by default on Haiku, where it is the system volume. (Disk, Haiku)
|
||||
* Improved Snapdragon X1/X2 model name detection on Linux. (#2625, CPU, Linux)
|
||||
* Fixed the Android build failing with `GL/gl.h` not found, because the NDK ships the EGL headers that turned OpenGL detection on but not the desktop GL headers it includes. (OpenGL, Android)
|
||||
* Fixed MAC address reporting `02:00:00:00:00:00` on macOS 27. (LocalIP, Apple)
|
||||
* Fixed assertion failure when percentage exceeds 100. (Percent)
|
||||
* Fixed a non-existent command being reported as a generic child process error on Windows instead of `command not found`. (Command, Windows)
|
||||
* Fixed Android display detection on devices running Android 17. (Display, Android)
|
||||
* Fixed Windows volumes with a per-user quota (such as network shares) reporting a wrong total size and a wrapped-around used size, because the total size was quota-limited while the free size was not. (#2651, Disk, Windows)
|
||||
* On Windows 10 1809 (build 17763) and later, the whole volume's capacity and free space are now reported by default. Set `disk.useAvailable` to `true` to keep reporting the space that is available to the current user, quota included.
|
||||
|
||||
Logos:
|
||||
* Added filled Artix logo (#2631)
|
||||
* Renamed PostmarketOS to nura (#2626)
|
||||
|
||||
# 2.69.0
|
||||
|
||||
Changes:
|
||||
* ImageMagick is no longer used for image logos on Windows, macOS and Android, and has been replaced by the platform image frameworks (WIC on Windows, ImageIO on macOS, AImageDecoder on Android). (Logo)
|
||||
* An image logo needs Android 11 (API 30), the release that introduced the platform image decoder, and is reported as an error on Android 10 and older.
|
||||
* ImageMagick 6 support is deprecated. It is kept only for old Debian and Ubuntu releases that don't have ImageMagick 7 available, and is intended to be removed in a future release. Users are encouraged to upgrade to ImageMagick 7 when possible.
|
||||
* The `--logo-recache` option has been replaced by `--logo-cache <bool|regen>`, and the `logo.recache` JSON property has been renamed to `logo.cache`. (Logo)
|
||||
* `--logo-cache true` (the default) reuses a cached rendering when it is valid, and writes it back on a cache miss; `false` ignores the image logo cache completely, reading nothing from it and writing nothing to it; `regen` does what `--logo-recache true` used to do. `logo.cache` accepts a boolean, or the string `"regen"`.
|
||||
* The `waitTime` option of `DiskIO` and `NetIO` now defaults to `250` ms instead of `500`. (DiskIO / NetIO)
|
||||
* The byte counters are maintained by the kernel as I/O happens, so a shorter sampling window still yields an accurate rate, and both modules now finish about 250 ms sooner. The two modules wait concurrently, so enabling both does not cost twice the wait time.
|
||||
* The `waitTime` option of `Top` now defaults to `250` ms instead of `500`. (Top)
|
||||
* The operating system accounts process CPU time in fixed steps, so a window longer than roughly 150 ms does not visibly improve the reported percentages. The module now finishes about 250 ms sooner.
|
||||
* ImageMagick 6 is deprecated and will be removed in a future release. It remains available for older Debian and Ubuntu releases; upgrade to ImageMagick 7 when possible.
|
||||
* The experimental QuickJS format scripting is deprecated, disabled by default, and planned for removal in the next release. Re-enable it with `cmake -DENABLE_QUICKJS=ON`, or migrate scripts to Lua. (General)
|
||||
* Replaced `--logo-recache` and `logo.recache` with `--logo-cache` and `logo.cache`. Use `true` to reuse or create valid cache entries, `false` to disable caching, or `regen` to force regeneration. (Logo)
|
||||
* Modules selected with `--structure` now honor their options from the JSON config.
|
||||
|
||||
Features:
|
||||
* Improved image logo support
|
||||
* Backend rewritten
|
||||
* Added a native image decoding backend on Windows (WIC), macOS (ImageIO) and Android (AImageDecoder).
|
||||
* Added an embedded libsixel encoder, used to produce sixel output on Windows, macOS and Android. It is reported by `fastfetch --list-features` as "Embedded sixel".
|
||||
* Enabled chafa image output on Windows, macOS and Android independently of ImageMagick.
|
||||
* As a result, `fastfetch --sixel X:\path\to\image` now works out of the box on Windows Terminal.
|
||||
* Image logo cache entries are now validated against the modification time of the source image, so editing an image logo in place invalidates its cached rendering. (Logo)
|
||||
* Cache entries written by older versions are not reused, as they carry no modification time.
|
||||
* Image logos can now be animated when the terminal and the image protocol support it. (Logo)
|
||||
* `--logo-animation-frame <0>` (`logo.animationFrame: 0` in the JSON config) plays a GIF or APNG. Only the `kitty` image protocol can play an animation; the frames are decoded and composed by fastfetch, so no external program is involved.
|
||||
* `--logo-animation-frame <N>` renders the Nth frame as a still image, and negative values count back from the end, so `-1` is the last frame. This works for the `sixel`, `kitty` and `chafa` logo types. Note that negative values can only be given in the JSON config, as the command line parser reads a leading `-` as another option.
|
||||
* The default is `1`, which renders a still image, so nothing changes for anyone who does not opt in. The frames come from the platform image framework (WIC on Windows, ImageIO on macOS, AImageDecoder on Android, ImageMagick 7 on Linux); a single-frame GIF falls back to a still image.
|
||||
* On Android an animation needs Android 12 (API 31), and the repeat count is not available there, so an animation is reported as looping forever. A build with no image decoder, or one built with ImageMagick 6, reports an error instead of silently showing a still image.
|
||||
* A terminal that supports the kitty graphics protocol but not its animation frames, such as Konsole, shows the first frame.
|
||||
* Added the CMake option `ENABLE_IMAGE_LOGO`, which defaults to `ON`. Configure with `-DENABLE_IMAGE_LOGO=OFF` to build fastfetch without any image logo support. (Logo)
|
||||
* Image logos are the only consumer of ImageMagick, chafa, and the embedded libsixel encoder, so none of the three is searched for at configure time, and no image decoding sources are compiled in. This is intended only to reduce binary size on embedded systems (such as OpenWrt).
|
||||
* The `sixel`, `kitty`, `kitty-direct`, `kitty-icat`, `iterm` and `chafa` logo types are rejected with an error, both on the command line and in the JSON config, and the `auto` logo type never tries an image.
|
||||
* `--logo-type raw` keeps working: it passes a pre-rendered byte stream through unchanged and needs no decoder, so a logo can still be displayed by converting the image externally.
|
||||
* Added CPU name and frequency detection support on SPARC. (CPU, Linux)
|
||||
* Added package detection support for CRUX. (Packages, Linux)
|
||||
* Exposed in custom format as `{crux}`.
|
||||
* Improved Android ROM detection (DE, Android)
|
||||
* Added support for HarmonyOS, HarmonyOS NEXT, Flyme, JOYUI, SmartisanOS, realme UI, HydrogenOS, ZUI, ZUXOS, MyOS, NebulaAIOS, ObricUI, MiFavor, LineageOS, PixelExperience, EUI and 360 UI.
|
||||
* Added support for MagicUI 3.x, which stores a bare version number instead of a `MagicUI_x.y.z` string.
|
||||
* Added Samsung OneUI support (#2541)
|
||||
* It is read from system properties, so no particular Android version is required. This is mostly untested due to lack of available devices running these ROMs. Please report any issues you encounter.
|
||||
* Improved Camera detection on Android (Camera, Android)
|
||||
* The camera list is now read from the camera2 NDK instead of `termux-api CameraInfo`, so the Termux:API app is no longer required and no subprocess is spawned.
|
||||
* This needs Android 7.0 (API 24), which is where the camera2 NDK was introduced.
|
||||
* Improved Battery detection on Android (Battery, Android)
|
||||
* The charge level and charging state are now read from the battery properties service over `/dev/binder` instead of `termux-api BatteryStatus`, so the Termux:API app is no longer required and no subprocess is spawned.
|
||||
* The service interface changed in Android 10, and the right request is picked at run time, so every Android release is covered.
|
||||
* Battery temperature, cycle count, manufacturer, model name, serial number and manufacture date need the `BATTERY_STATS` permission, which an app cannot obtain, so an app no longer reports them.
|
||||
* Running as `adb shell` or as root, `dumpsys battery` is used, which also reports the battery temperature and technology, and reports a battery that has reached the critical level as such. An app can read none of the three.
|
||||
* Improved Display detection on Android (Display, Android)
|
||||
* The displays are now read from the display service instead of a vendor property that only some Xiaomi devices set. The preferred mode, the physical size, the rotation, the manufacture date and the display id are now reported as well.
|
||||
* This needs Android 13 (API 33). On Android 12 and older only that vendor property is available to an app, and a device that does not set it reports no display.
|
||||
* Running as `adb shell` or as root, `dumpsys display` is used as well, which is what covers Android 12 and older.
|
||||
* The refresh rate is now the rate of the active display mode, and the HDR capability is read from the display itself, for every display rather than only for the built-in one.
|
||||
* Improved WiFi detection on Android (Wifi, Android)
|
||||
* The connection details are now read from the WiFi service over `/dev/binder` instead of `termux-api WifiConnectionInfo`, so no subprocess is spawned.
|
||||
* The Termux:API app is still required, though. It is what carries the WiFi permission that the Termux app does not request itself, so uninstalling it turns this module off.
|
||||
* A location permission is required as well, and Termux:API is what carries that one too. The WiFi service withholds the SSID and the BSSID from an app that does not hold one, and hands both over once it is granted; until then the signal, the rates, the frequency, the Wi-Fi standard and the security type are reported as usual, and the two names are reported as `<redacted>`. The grant has to be for all the time rather than only while the app is in use, as the service withholds the two names from an app that is not in the foreground either -- which is the state a run over `ssh` is in.
|
||||
* The interface name and its state, the connection state, the Wi-Fi standard and the security type are now reported as well.
|
||||
* The security type is reported from Android 12 on. The reply Android 11 sends ends before that field, so the `security` field stays empty there.
|
||||
* This needs Android 11 (API 30), the release that moved the Wi-Fi framework into an APEX. There is no fallback, so Android 10 and older report an error instead.
|
||||
* WiFi detection on Android is experimental. The reply the WiFi service hands an app drifts between Android releases and between vendors, and what it contains also depends on the permissions the calling app holds, so a device can report less than its connection has. Please report anything that looks wrong or missing.
|
||||
* Improved COSMIC detection (DE / WM, Linux)
|
||||
* The version is now read from the `COSMIC_VERSION` environment variable when it is set.
|
||||
* Improved accuracy and performance of process name detection in the Top module. (Top, macOS)
|
||||
* Improved Packages detection on Windows (Packages, Windows)
|
||||
* `winget` is now detected by default, instead of requiring a build-time opt-in, and the source agreements are accepted non-interactively on a machine that has never accepted them. Opt out by setting `packages.disabled` to `["winget"]`, or by configuring with `cmake -DPACKAGES_DISABLE_WINGET=ON`.
|
||||
* The count reports the packages the `winget` source knows about, which is not limited to the packages `winget` itself installed. Microsoft Store apps are no longer counted.
|
||||
* `winget list` takes about 1.5 seconds, so its result is cached. The cache is refreshed when a program is installed or removed, not on a timer, so only the first run after such a change is slow.
|
||||
* Improved Wallpaper detection on macOS Sonoma and later (#2559, Wallpaper, macOS)
|
||||
* The image path is now also extracted from the `Configuration` field of the wallpaper plist, and the `NSWorkspace` fallback is used only as a last resort.
|
||||
* Removed the `kvm` dependency on OpenBSD by using `sysctl` directly. (General, OpenBSD)
|
||||
* Modules that were selected on the command line via `--structure` / `-s` now honor module options configured in the JSON config. (CommandOption)
|
||||
* Improved reliability of fastfetch's built-in HTTP client. (PublicIP, Weather)
|
||||
* It now supports custom ports and can properly handle chunked transfer encoding. It is designed for minimal resource usage and fast performance, and does not support full HTTP features like HTTPS; the `Command` module with `curl` can be used for those.
|
||||
* Added Umbriel wayland compositor version detection (WM, Linux)
|
||||
* Improved the player name detection on Windows to show the name Windows shows for it. (Player, Windows)
|
||||
* An unpackaged player such as Chrome is now reported as `Google Chrome` instead of `Chrome`.
|
||||
* Added `logo.position: "auto"` and `--logo-position auto` to place wide logos above module output when the terminal has less than 32 columns available for text. (Logo)
|
||||
* The position can also be selected in interactive `--gen-config`.
|
||||
* For now, the default position is `left` for compatibility with previous releases. This may change to `auto` in future releases.
|
||||
* Improved Bluetooth detection support:
|
||||
* Added Bluetooth Low Energy detection on Windows and macOS.
|
||||
* Report device type (LE or classic) and signal quality, if available.
|
||||
* Added Bluetooth Core 6.0–6.3 version reporting. (BluetoothRadio)
|
||||
* Improved image log support:
|
||||
* Image logos on Windows, macOS, and Android now use native image processing libraries instead of ImageMagick.
|
||||
* For package managers: ImageMagick dependencies can be removed on macOS, Windows and Android as they are no longer used for image logos.
|
||||
* Windows Terminal supports sixel logos out of the box
|
||||
* Changes to a source image now correctly invalidate its cache. (Logo)
|
||||
* Added GIF and APNG support to `--kitty` image protocol. Set `logo.animationFrame` to `0` to play animations in compatible kitty terminals; positive or negative values select a still frame. Android animation requires Android 12 (API 31); terminals without kitty graphics protocol support report an error and fall back to the built-in logo instead. (Logo)
|
||||
* Added cmake option `-DENABLE_IMAGE_LOGO=<BOOL>` (default `ON`) to disable image logos and reduce binary size. `raw` logos remain available. (Logo)
|
||||
* Improved Linux support:
|
||||
* Added RakuOS rum overlay package counting (`{rum}`) and CRUX package detection (`{crux}`). (Packages)
|
||||
* Added CPU name and frequency detection on SPARC. (CPU)
|
||||
* Improved COSMIC version detection and added Umbriel version detection. (DE / WM)
|
||||
* Improved Android support:
|
||||
* Expanded ROM detection to HarmonyOS, Flyme, OneUI, LineageOS, and many other ROMs. (DE)
|
||||
* Improved camera detection; Android 7 (API 24) or later is required. (Camera)
|
||||
* Battery level and charging state are now available to apps; detailed battery information requires root or ADB. (Battery)
|
||||
* Added display mode, physical size, rotation, and HDR information. App-based detection requires Android 13 (API 33); root or ADB can also detect displays on older versions. (Display)
|
||||
* Added Wi-Fi interface, connection, standard, and security information. Android 11 (API 30) or later is required. Termux:API is needed for the Wi-Fi permission, and location permission is needed to reveal SSID and BSSID; without location permission the two names appear as `<redacted>`. (Wifi)
|
||||
* Added Wallpaper detection, which effectively reports `/data/system/users/0/wallpaper`. (Wallpaper)
|
||||
* Added Media detection, root/ADB only. (Media / Player)
|
||||
* Improved macOS support:
|
||||
* Improved process-name detection in Top. (Top)
|
||||
* Improved wallpaper detection on macOS Sonoma and later. (#2559, Wallpaper)
|
||||
* Improved Windows support:
|
||||
* `winget` now counts packages from configured sources, excludes Microsoft Store apps, and caches results. It remains disabled by default. (Packages)
|
||||
* Player names now match the names shown by Windows (for example, Chrome is reported as Google Chrome). (Player)
|
||||
* Added battery-level detection for more controllers on Windows and macOS. (Gamepad)
|
||||
* Improved macOS controller name detection (macOS, Gamepad)
|
||||
* macOS controller names now match the system; configs that filter by name may need updating.
|
||||
* Requires macOS 11 or later.
|
||||
* Improved the built-in HTTP client used by PublicIP and Weather; it now supports custom ports and chunked responses. (PublicIP / Weather)
|
||||
* Reduced the default `waitTime` for DiskIO, NetIO, and Top from 500 ms to 250 ms. (DiskIO / NetIO / Top)
|
||||
* Added `general.preload.lua` for loading shared Lua helpers before modules run. It requires a Lua-enabled build and starts the Lua interpreter whenever configured. (General)
|
||||
* Lua format scripting is now considered stable.
|
||||
|
||||
Bugfixes:
|
||||
* Fixed Base64 encoding producing incorrect output for some inputs. (General)
|
||||
* Fixed image logos not working when ImageMagick is built without a quantum depth suffix in its library name, as is the case on FreeBSD. (Logo, FreeBSD)
|
||||
* Fixed TerminalFont detection on Windows ignoring Windows Terminal JSON fragment files. (#2573, TerminalFont, Windows)
|
||||
* Fixed 64-bit values being truncated by `strtoul` on platforms where `unsigned long` is 32-bit. (Swap / PhysicalDisk / PhysicalMemory / GPU)
|
||||
* Fixed read-only SQLite databases failing with `SQLITE_READONLY` when the database directory is not writable. (Packages)
|
||||
* This fixes PKG package count detection on FreeBSD.
|
||||
* Fixed `{#keys}` and `{#title}` in module format strings not honoring the `brightColor` display option. (Format)
|
||||
* Fixed `paddingTop` and `paddingLeft` being ignored by the `kitty-icat` image logo type. (Logo)
|
||||
* Fixed issues when running on big-endian platforms.
|
||||
* Some internal cleanups and optimizations.
|
||||
* Fixed Snapdragon X2 series model detection on Linux. (#2611, CPU / Linux)
|
||||
* Fixed image logo caching, padding, and positioning issues, including an iTerm display bug. (Logo)
|
||||
* Fixed several Windows issues, including Windows Terminal font detection, redirected `--gen-config` output, and gamepad battery reporting for DualShock 4 and Switch controllers. (#2573, TerminalFont; Gamepad; General)
|
||||
* Fixed formatting and display issues, including ANSI-aware truncation, date/time formatting, and bright key/title colors. (Format / DateTime / Display)
|
||||
* Fixed `--dynamic-interval` retries and stale results across multiple rounds. (PublicIP / Weather / Display / Monitor / WM / DE / Media / Player / Shell / Terminal)
|
||||
* Fixed invalid numeric and color options that could cause hangs or excessive output. (Separator / CPUUsage / NetIO / DiskIO / LoadAvg / Colors)
|
||||
* Fixed JSON output and config validation across several modules, including GPU, Btrfs, Gamepad, Packages, and Custom. (General)
|
||||
* Fixed GPU memory and device-type reporting, GNOME Classic and Trinity version detection, and default-route family names. (GPU / DE / LocalIP)
|
||||
* Fixed module output and filtering issues affecting editor failures, sound status, command selection, and ignored keyboard/gamepad devices. (Editor / Sound / Command / Keyboard / Gamepad)
|
||||
* Fixed package counting with read-only databases and corrected other package detection and JSON output issues. (Packages)
|
||||
* Fixed Base64 encoding, big-endian handling, and 64-bit value truncation on 32-bit platforms. (General)
|
||||
|
||||
Logos:
|
||||
* Added ALT Atomic
|
||||
|
||||
+228
-49
@@ -1,7 +1,7 @@
|
||||
cmake_minimum_required(VERSION 3.21.0) # C_STANDARD C17 and C23
|
||||
|
||||
project(fastfetch
|
||||
VERSION 2.69.0
|
||||
VERSION 2.70.0
|
||||
LANGUAGES C
|
||||
DESCRIPTION "Fast neofetch-like system information tool"
|
||||
HOMEPAGE_URL "https://github.com/fastfetch-cli/fastfetch"
|
||||
@@ -60,6 +60,7 @@ endif()
|
||||
|
||||
include(CheckIncludeFile)
|
||||
include(CheckFunctionExists)
|
||||
include(CheckSymbolExists)
|
||||
|
||||
if(LINUX)
|
||||
CHECK_INCLUDE_FILE("linux/version.h" HAVE_LINUX_VERSION_H)
|
||||
@@ -92,18 +93,20 @@ cmake_dependent_option(ENABLE_EGL "Enable egl" ON "LINUX OR FreeBSD OR OpenBSD O
|
||||
cmake_dependent_option(ENABLE_GLX "Enable glx" ON "LINUX OR FreeBSD OR OpenBSD OR NetBSD OR ANDROID OR SunOS OR GNU" OFF)
|
||||
cmake_dependent_option(ENABLE_OPENCL "Enable opencl" ON "LINUX OR FreeBSD OR OpenBSD OR NetBSD OR WIN32 OR ANDROID OR SunOS OR Haiku OR GNU" OFF)
|
||||
cmake_dependent_option(ENABLE_FREETYPE "Enable freetype" ON "ANDROID" OFF)
|
||||
cmake_dependent_option(ENABLE_PULSE "Enable pulse" ON "LINUX OR ANDROID OR GNU" OFF)
|
||||
cmake_dependent_option(ENABLE_DDCUTIL "Enable ddcutil" ON "LINUX" OFF)
|
||||
cmake_dependent_option(ENABLE_ELF "Enable libelf" ON "LINUX OR ANDROID OR DragonFly OR Haiku OR GNU" OFF)
|
||||
cmake_dependent_option(ENABLE_THREADS "Enable multithreading" ON "Threads_FOUND" OFF)
|
||||
|
||||
option(ENABLE_IMAGE_LOGO "Enable image logos (sixel / kitty / iTerm / chafa)" ON)
|
||||
if(ENABLE_IMAGE_LOGO)
|
||||
cmake_dependent_option(ENABLE_IMAGEMAGICK7 "Enable imagemagick 7" ON "LINUX OR FreeBSD OR OpenBSD OR NetBSD OR SunOS OR Haiku OR GNU" OFF)
|
||||
cmake_dependent_option(ENABLE_IMAGEMAGICK6 "Enable imagemagick 6" ON "LINUX OR FreeBSD OR OpenBSD OR NetBSD OR SunOS OR GNU" OFF)
|
||||
cmake_dependent_option(ENABLE_SIXEL "Enable sixel logo output via the embedded libsixel encoder (Windows, macOS and Android; replaces ImageMagick's SIXEL coder)" ON "WIN32 OR APPLE OR ANDROID" OFF)
|
||||
cmake_dependent_option(ENABLE_CHAFA "Enable chafa" ON "ENABLE_IMAGEMAGICK6 OR ENABLE_IMAGEMAGICK7 OR WIN32 OR APPLE OR ANDROID" OFF)
|
||||
endif()
|
||||
# The four below are declared outside the guard on purpose. `cmake_dependent_option` is a macro: when
|
||||
# the condition is false it is never expanded, so nothing rewrites the value already sitting in
|
||||
# CMakeCache.txt, and `cmake -DENABLE_IMAGE_LOGO=OFF .` in an existing build directory would leave
|
||||
# ENABLE_SIXEL=ON behind -- sixel and chafa would still be compiled and linked while image.c, the
|
||||
# only file that uses them, is not. The condition does the disabling instead: it is the fourth
|
||||
# argument's job, and it keeps the option visible and settable in cmake-gui either way.
|
||||
cmake_dependent_option(ENABLE_IMAGEMAGICK7 "Enable imagemagick 7" ON "ENABLE_IMAGE_LOGO;LINUX OR FreeBSD OR OpenBSD OR NetBSD OR SunOS OR Haiku OR GNU" OFF)
|
||||
cmake_dependent_option(ENABLE_IMAGEMAGICK6 "Enable imagemagick 6" ON "ENABLE_IMAGE_LOGO;LINUX OR FreeBSD OR OpenBSD OR NetBSD OR SunOS OR GNU" OFF)
|
||||
cmake_dependent_option(ENABLE_SIXEL "Enable sixel logo output via the embedded libsixel encoder (Windows, macOS and Android; replaces ImageMagick's SIXEL coder)" ON "ENABLE_IMAGE_LOGO;WIN32 OR APPLE OR ANDROID" OFF)
|
||||
cmake_dependent_option(ENABLE_CHAFA "Enable chafa" ON "ENABLE_IMAGE_LOGO;ENABLE_IMAGEMAGICK6 OR ENABLE_IMAGEMAGICK7 OR WIN32 OR APPLE OR ANDROID" OFF)
|
||||
|
||||
option(ENABLE_ZLIB "Enable zlib" ON)
|
||||
option(ENABLE_SYSTEM_YYJSON "Use system provided (instead of fastfetch embedded) yyjson library" OFF)
|
||||
@@ -119,7 +122,6 @@ option(ENABLE_EMBEDDED_PCIIDS "Embed pci.ids into fastfetch, requires `python`"
|
||||
option(ENABLE_EMBEDDED_AMDGPUIDS "Embed amdgpu.ids into fastfetch, requires `python`" OFF)
|
||||
option(ENABLE_WORDEXP "Enable using of wordexp(3) if available, instead of glob(3)" ON)
|
||||
option(ENABLE_LUA "Enable lua scripting for format strings" ON)
|
||||
option(ENABLE_QUICKJS "Enable quickjs scripting for format strings" ON)
|
||||
option(ENABLE_LIBZFS "Enable libzfs" ON)
|
||||
option(ENABLE_WCWIDTH "Detect wide-width characters with wcwidth when calculating logo and separator width. Disable for a smaller and slightly faster build, but logos containing CJK/emoji may be measured too narrow and render incorrectly. Such usecases are uncommon." ON)
|
||||
set(DEFAULT_STRUCTURE "Title:Separator:OS:Host:Kernel:Uptime:Packages:Shell:Display:DE:WM:WMTheme:Theme:Icons:Font:Cursor:Terminal:TerminalFont:CPU:GPU:Memory:Swap:Disk:LocalIp:Battery:PowerAdapter:Locale:Break:Colors" CACHE STRING "Default module structure for detection")
|
||||
@@ -135,15 +137,30 @@ endif()
|
||||
# other leftovers, and makes `CONFIGURE_DEPENDS` react to added or removed sources.
|
||||
file(GLOB FF_MODULE_SRCS CONFIGURE_DEPENDS RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/src/modules" "${CMAKE_CURRENT_SOURCE_DIR}/src/modules/*/*.c")
|
||||
set(FF_MODULE_DIRS "")
|
||||
set(FF_MODULE_SRC_DIRS "")
|
||||
foreach(FF_MODULE_SRC ${FF_MODULE_SRCS})
|
||||
get_filename_component(FF_MODULE_DIR "${FF_MODULE_SRC}" DIRECTORY)
|
||||
get_filename_component(FF_MODULE_NAME "${FF_MODULE_SRC}" NAME_WE)
|
||||
if("${FF_MODULE_DIR}" STREQUAL "${FF_MODULE_NAME}")
|
||||
list(APPEND FF_MODULE_DIRS "${FF_MODULE_DIR}")
|
||||
endif()
|
||||
list(APPEND FF_MODULE_SRC_DIRS "${FF_MODULE_DIR}")
|
||||
endforeach()
|
||||
unset(FF_MODULE_SRCS)
|
||||
|
||||
# A directory of sources whose name no source carries registers no module at all, and nothing else
|
||||
# in the build says so -- the module simply never appears. Reported rather than left to be noticed.
|
||||
# Directories are compared rather than sources, so that a module which keeps helper sources next to
|
||||
# its own `<module>.c` does not warn about every one of them.
|
||||
list(REMOVE_DUPLICATES FF_MODULE_SRC_DIRS)
|
||||
foreach(FF_MODULE_DIR ${FF_MODULE_SRC_DIRS})
|
||||
if(NOT FF_MODULE_DIR IN_LIST FF_MODULE_DIRS)
|
||||
message(WARNING "src/modules/${FF_MODULE_DIR}/ contains C sources but no ${FF_MODULE_DIR}.c, "
|
||||
"so no module is registered for it. A module directory must hold a source file named after it.")
|
||||
endif()
|
||||
endforeach()
|
||||
unset(FF_MODULE_SRC_DIRS)
|
||||
|
||||
foreach(FF_MODULE_DIR ${FF_MODULE_DIRS})
|
||||
string(TOUPPER "${FF_MODULE_DIR}" FF_MODULE_UPPER)
|
||||
option(MODULE_DISABLE_${FF_MODULE_UPPER} "Disable module ${FF_MODULE_DIR}" OFF)
|
||||
@@ -161,7 +178,11 @@ string(REGEX MATCHALL "FF_PACKAGES_FLAG_[A-Z]+_BIT" PACKAGE_MANAGERS "${PACKAGE_
|
||||
string(REGEX REPLACE "FF_PACKAGES_FLAG_([A-Z]+)_BIT" "\\1" PACKAGE_MANAGERS "${PACKAGE_MANAGERS}")
|
||||
list(SORT PACKAGE_MANAGERS)
|
||||
foreach(package_manager ${PACKAGE_MANAGERS})
|
||||
option(PACKAGES_DISABLE_${package_manager} "Disable ${package_manager} package manager detection by default" OFF)
|
||||
if(package_manager STREQUAL "WINGET")
|
||||
option(PACKAGES_DISABLE_${package_manager} "Disable ${package_manager} package manager detection by default" ON)
|
||||
else()
|
||||
option(PACKAGES_DISABLE_${package_manager} "Disable ${package_manager} package manager detection by default" OFF)
|
||||
endif()
|
||||
endforeach()
|
||||
option(PACKAGES_REMOVE_DISABLED "Remove disabled package managers from the build entirely, instead of just skipping them at runtime" OFF)
|
||||
|
||||
@@ -260,6 +281,22 @@ if(ENABLE_LTO AND NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Release and MinSizeRel drop the symbol table and the unwind metadata; Debug and RelWithDebInfo keep
|
||||
# both, so those stay fully debuggable. CMake does not strip MinSizeRel on its own.
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel")
|
||||
if(NOT APPLE) # ld: warning: -s is obsolete
|
||||
# The symbol table is dead weight: 437 KB (16%) of a MinGW build, 91 KB (3%) of a GCC build.
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-s")
|
||||
endif()
|
||||
|
||||
# Drops .eh_frame / .eh_frame_hdr on ELF (74 KB) and .pdata / .xdata on PE (26 KB). Every C++
|
||||
# translation unit here is built with -fno-exceptions -fno-rtti and no source file contains a
|
||||
# single try / catch, so no exception can ever unwind through these frames. The only loss is
|
||||
# backtrace() / gdb / perf / crash-dump stack walking in a release binary.
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fno-asynchronous-unwind-tables")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-asynchronous-unwind-tables")
|
||||
endif()
|
||||
|
||||
#######################
|
||||
# Target FS structure #
|
||||
#######################
|
||||
@@ -417,7 +454,29 @@ fastfetch_encode_c_string("${DATATEXT_JSON_HELP}" DATATEXT_JSON_HELP)
|
||||
configure_file(src/fastfetch_config.h.in fastfetch_config.h @ONLY)
|
||||
configure_file(src/fastfetch_datatext.h.in fastfetch_datatext.h @ONLY)
|
||||
if(APPLE)
|
||||
# `mediaremoted` serves now-playing data only to clients whose bundle id starts
|
||||
# with "com.apple.": -[MRDMediaRemoteClient isAllowedAccessToDataFromPlayerPath:]
|
||||
# accepts the 0x200 entitlement bit either for the private
|
||||
# com.apple.mediaremote.now-playing-read-access entitlement or for that prefix,
|
||||
# and it derives the client's identity from the code signature / XPC audit token
|
||||
# rather than from the process's own NSBundle. Both the embedded
|
||||
# CFBundleIdentifier and the ad-hoc signing identifier below therefore have to
|
||||
# carry the prefix; keep them in sync through these variables.
|
||||
set(FASTFETCH_BUNDLE_ID "com.apple.fastfetch")
|
||||
set(FLASHFETCH_BUNDLE_ID "com.apple.flashfetch")
|
||||
# configure_file() inputs are not configure dependencies on their own, so
|
||||
# without this an edit to Info.plist.in never even regenerates Info.plist.
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/common/apple/Info.plist.in")
|
||||
# Each executable gets its own plist: the file is embedded with -sectcreate and read
|
||||
# back by the system as the bundle id, so two targets sharing one would leave one of
|
||||
# them advertising an identifier that its code signature does not match.
|
||||
set(BUNDLE_ID "${FASTFETCH_BUNDLE_ID}")
|
||||
configure_file(src/common/apple/Info.plist.in Info.plist @ONLY)
|
||||
if(BUILD_FLASHFETCH)
|
||||
set(BUNDLE_ID "${FLASHFETCH_BUNDLE_ID}")
|
||||
configure_file(src/common/apple/Info.plist.in Info.plist.flashfetch @ONLY)
|
||||
endif()
|
||||
unset(BUNDLE_ID)
|
||||
endif()
|
||||
|
||||
####################
|
||||
@@ -440,6 +499,7 @@ file(GENERATE OUTPUT logo_builtin.h CONTENT "${LOGO_BUILTIN_H}")
|
||||
|
||||
set(LIBFASTFETCH_SRC
|
||||
src/common/impl/commandoption.c
|
||||
src/common/impl/debug.c
|
||||
src/common/impl/duration.c
|
||||
src/common/impl/font.c
|
||||
src/common/impl/format.c
|
||||
@@ -462,12 +522,14 @@ set(LIBFASTFETCH_SRC
|
||||
src/common/impl/time.c
|
||||
src/common/impl/edidHelper.c
|
||||
src/common/impl/base64.c
|
||||
src/common/impl/FFcache.c
|
||||
src/common/impl/FFlist.c
|
||||
src/common/impl/FFstrbuf.c
|
||||
src/common/impl/path.c
|
||||
src/common/impl/FFPlatform.c
|
||||
src/common/impl/smbios.c
|
||||
src/common/impl/strutil.c
|
||||
src/common/impl/zlib.c
|
||||
src/detection/bluetoothradio/bluetoothradio.c
|
||||
src/detection/bootmgr/bootmgr.c
|
||||
src/detection/chassis/chassis.c
|
||||
@@ -515,6 +577,11 @@ if(ENABLE_IMAGE_LOGO)
|
||||
src/logo/image/im7.c
|
||||
src/logo/image/sixel.c
|
||||
)
|
||||
if(ANDROID)
|
||||
list(APPEND LIBFASTFETCH_SRC
|
||||
src/logo/image/aid.c
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
foreach(FF_MODULE_DIR ${FF_MODULE_DIRS})
|
||||
@@ -611,8 +678,11 @@ if(LINUX)
|
||||
)
|
||||
elseif(ANDROID)
|
||||
list(APPEND LIBFASTFETCH_SRC
|
||||
src/common/android/applabel.c
|
||||
src/common/android/binder.c
|
||||
src/common/android/dex.c
|
||||
src/common/android/package.c
|
||||
src/common/android/zip.c
|
||||
src/common/impl/dbus.c
|
||||
src/common/impl/io_unix.c
|
||||
src/common/impl/netif_linux.c
|
||||
@@ -666,6 +736,7 @@ elseif(ANDROID)
|
||||
src/detection/locale/locale_linux.c
|
||||
src/detection/localip/localip_linux.c
|
||||
src/detection/gamepad/gamepad_nosupport.c
|
||||
src/detection/media/media_android.c
|
||||
src/detection/media/media_linux.c
|
||||
src/detection/memory/memory_linux.c
|
||||
src/detection/mouse/mouse_nosupport.c
|
||||
@@ -676,6 +747,7 @@ elseif(ANDROID)
|
||||
src/detection/packages/packages_nix.c
|
||||
src/detection/poweradapter/poweradapter_nosupport.c
|
||||
src/detection/processes/processes_linux.c
|
||||
src/detection/sound/sound_android.c
|
||||
src/detection/sound/sound_linux.c
|
||||
src/detection/swap/swap_linux.c
|
||||
src/detection/terminalfont/terminalfont_android.c
|
||||
@@ -687,13 +759,13 @@ elseif(ANDROID)
|
||||
src/detection/tpm/tpm_nosupport.c
|
||||
src/detection/uptime/uptime_linux.c
|
||||
src/detection/users/users_linux.c
|
||||
src/detection/wallpaper/wallpaper_android.c
|
||||
src/detection/wallpaper/wallpaper_linux.c
|
||||
src/detection/wifi/wifi_android.c
|
||||
src/detection/wm/wm_linux.c
|
||||
src/detection/de/de_linux.c
|
||||
src/detection/wmtheme/wmtheme_linux.c
|
||||
src/detection/camera/camera_android.c
|
||||
src/logo/image/aid.c
|
||||
)
|
||||
elseif(FreeBSD)
|
||||
list(APPEND LIBFASTFETCH_SRC
|
||||
@@ -1004,7 +1076,8 @@ elseif(APPLE)
|
||||
src/detection/libc/libc_apple.c
|
||||
src/detection/locale/locale_linux.c
|
||||
src/detection/localip/localip_linux.c
|
||||
src/detection/gamepad/gamepad_apple.c
|
||||
src/detection/localip/localip_apple.c
|
||||
src/detection/gamepad/gamepad_apple.m
|
||||
src/detection/media/media_apple.m
|
||||
src/detection/memory/memory_apple.c
|
||||
src/detection/mouse/mouse_apple.c
|
||||
@@ -1063,17 +1136,19 @@ elseif(WIN32)
|
||||
src/common/impl/processing_windows.c
|
||||
src/common/impl/FFPlatform_windows.c
|
||||
src/common/impl/binary_windows.c
|
||||
src/common/impl/debug_windows.c
|
||||
src/common/impl/kmod_windows.c
|
||||
src/common/windows/getline.c
|
||||
src/common/windows/com.c
|
||||
src/common/windows/folders.c
|
||||
src/common/windows/registry.c
|
||||
src/common/windows/unicode.c
|
||||
src/common/windows/variant.cpp
|
||||
src/common/windows/winrt.cpp
|
||||
src/common/windows/version.c
|
||||
src/detection/battery/battery_windows.c
|
||||
src/detection/bios/bios_windows.c
|
||||
src/detection/bluetooth/bluetooth_windows.c
|
||||
src/detection/bluetooth/bluetooth_windows.cpp
|
||||
src/detection/bluetoothradio/bluetoothradio_windows.c
|
||||
src/detection/board/board_windows.c
|
||||
src/detection/bootmgr/bootmgr_windows.c
|
||||
@@ -1342,7 +1417,7 @@ elseif(GNU)
|
||||
src/detection/keyboard/keyboard_nosupport.c
|
||||
src/detection/libc/libc_linux.c
|
||||
src/detection/lm/lm_linux.c
|
||||
src/detection/loadavg/loadavg_linux.c
|
||||
src/detection/loadavg/loadavg_sunos.c
|
||||
src/detection/locale/locale_linux.c
|
||||
src/detection/localip/localip_linux.c
|
||||
src/detection/gamepad/gamepad_nosupport.c
|
||||
@@ -1500,11 +1575,15 @@ if(APPLE AND BINARY_LINK_TYPE STREQUAL "dlopen")
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-rpath,/opt/homebrew/lib -Wl,-rpath,/usr/local/lib -Wl,-rpath,/opt/local/lib")
|
||||
endif()
|
||||
|
||||
# Termux installs the libraries fastfetch dlopens -- freetype among them -- under TARGET_DIR_ROOT, which
|
||||
# is not on the Android linker's default search path (DT_RUNPATH, LD_LIBRARY_PATH and the default
|
||||
# namespace). Termux's own clang injects -rpath=$PREFIX/lib into every binary it links, so a build made
|
||||
# inside Termux finds them; a plain NDK build, which is what the CI does, does not.
|
||||
if(ANDROID)
|
||||
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-rpath,/vendor/lib64 -Wl,-rpath,/system/lib64")
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-rpath,${TARGET_DIR_ROOT}/lib -Wl,-rpath,/vendor/lib64 -Wl,-rpath,/system/lib64")
|
||||
else()
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-rpath,/vendor/lib -Wl,-rpath,/system/lib")
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-rpath,${TARGET_DIR_ROOT}/lib -Wl,-rpath,/vendor/lib -Wl,-rpath,/system/lib")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -1612,6 +1691,13 @@ if(FreeBSD OR OpenBSD OR NetBSD)
|
||||
unset(CMAKE_REQUIRED_DEFINITIONS)
|
||||
endif()
|
||||
|
||||
if(OpenBSD)
|
||||
check_symbol_exists("getexecpath" "unistd.h" HAVE_GETEXECPATH)
|
||||
if(HAVE_GETEXECPATH)
|
||||
target_compile_definitions(libfastfetch PUBLIC FF_HAVE_GETEXECPATH=1)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(HAVE_PIPE2)
|
||||
target_compile_definitions(libfastfetch PUBLIC FF_HAVE_PIPE2)
|
||||
endif()
|
||||
@@ -1687,9 +1773,18 @@ function(ff_lib_enable VARNAME PKGCONFIG_NAMES CMAKE_NAME) # [CMAKE_TARGET_NAME]
|
||||
endif()
|
||||
endif()
|
||||
else()
|
||||
set(${VARNAME}_INCLUDE_DIRS ${${CMAKE_NAME}_INCLUDE_DIRS})
|
||||
set(${VARNAME}_LIBRARIES ${${CMAKE_NAME}_LIBRARIES})
|
||||
set(${VARNAME}_CFLAGS_OTHER ${${CMAKE_NAME}_CFLAGS_OTHER})
|
||||
# find_package modules disagree on how they name their result variables. Newer ones
|
||||
# export <PackageName>_* (FindVulkan sets Vulkan_INCLUDE_DIRS), but older ones only
|
||||
# set the upper-case form: FindFreetype sets FREETYPE_INCLUDE_DIRS and
|
||||
# FREETYPE_LIBRARIES and has no Freetype_INCLUDE_DIRS at all. Assigning
|
||||
# unconditionally would therefore wipe the include path the module had just
|
||||
# provided, and the package would be "found" with nothing to compile against.
|
||||
# Fill in only the gaps.
|
||||
foreach(_FF_SUFFIX INCLUDE_DIRS LIBRARIES CFLAGS_OTHER)
|
||||
if(NOT DEFINED ${VARNAME}_${_FF_SUFFIX} OR "${${VARNAME}_${_FF_SUFFIX}}" STREQUAL "")
|
||||
set(${VARNAME}_${_FF_SUFFIX} ${${CMAKE_NAME}_${_FF_SUFFIX}})
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
@@ -1808,27 +1903,16 @@ ff_lib_enable(OPENCL
|
||||
"OpenCL"
|
||||
"OpenCL"
|
||||
)
|
||||
# `Freetype` is the module CMake ships (Modules/FindFreetype.cmake); there is no FindFreeType2, so
|
||||
# this name never resolved and only pkg-config could ever turn freetype on.
|
||||
ff_lib_enable(FREETYPE
|
||||
"freetype2"
|
||||
"FreeType2"
|
||||
)
|
||||
ff_lib_enable(PULSE
|
||||
"libpulse"
|
||||
"Pulse"
|
||||
)
|
||||
ff_lib_enable(DDCUTIL
|
||||
"ddcutil"
|
||||
"Ddcutil"
|
||||
"Freetype"
|
||||
)
|
||||
ff_lib_enable(ELF
|
||||
"libelf"
|
||||
"libelf"
|
||||
)
|
||||
ff_lib_enable(QUICKJS
|
||||
"qjs"
|
||||
"qjs"
|
||||
"qjs"
|
||||
)
|
||||
if(ENABLE_LUA)
|
||||
# Lua is a special case since its CMake config does not follow the usual pattern
|
||||
find_package(Lua QUIET)
|
||||
@@ -1903,26 +1987,28 @@ if(LINUX)
|
||||
elseif(APPLE)
|
||||
target_link_libraries(libfastfetch
|
||||
PRIVATE "-framework AVFoundation"
|
||||
PRIVATE "-framework Accelerate"
|
||||
PRIVATE "-framework Cocoa"
|
||||
PRIVATE "-framework CoreFoundation"
|
||||
PRIVATE "-framework CoreAudio"
|
||||
PRIVATE "-framework CoreBluetooth"
|
||||
PRIVATE "-framework CoreFoundation"
|
||||
PRIVATE "-framework CoreMedia"
|
||||
PRIVATE "-framework CoreVideo"
|
||||
PRIVATE "-framework ImageIO"
|
||||
PRIVATE "-framework Accelerate"
|
||||
PRIVATE "-framework CoreWLAN"
|
||||
PRIVATE "-framework GameController"
|
||||
PRIVATE "-framework IOBluetooth"
|
||||
PRIVATE "-framework IOKit"
|
||||
PRIVATE "-framework ImageIO"
|
||||
PRIVATE "-framework Metal"
|
||||
PRIVATE "-framework OpenGL"
|
||||
PRIVATE "-framework OpenCL"
|
||||
PRIVATE "-framework OpenGL"
|
||||
PRIVATE "-framework SystemConfiguration"
|
||||
PRIVATE "-framework VideoToolbox"
|
||||
PRIVATE "-weak_framework CoreDisplay"
|
||||
|
||||
PRIVATE "-F /System/Library/PrivateFrameworks"
|
||||
PRIVATE "-weak_framework DisplayServices"
|
||||
PRIVATE "-weak_framework MediaRemote"
|
||||
PRIVATE "-weak_framework SkyLight"
|
||||
)
|
||||
elseif(WIN32)
|
||||
target_link_libraries(libfastfetch
|
||||
@@ -1939,8 +2025,16 @@ elseif(WIN32)
|
||||
PRIVATE "winbrand"
|
||||
PRIVATE "secur32"
|
||||
PRIVATE "shlwapi"
|
||||
PRIVATE "windowscodecs"
|
||||
)
|
||||
if(ENABLE_IMAGE_LOGO)
|
||||
# Only src/logo/image/wic.cpp uses WIC, and that file is only compiled with image logos
|
||||
# enabled (see the WIN32 source list above). Verified by building with
|
||||
# `-DENABLE_IMAGE_LOGO=OFF` and removing the library from the link line: it links and runs
|
||||
# without it, and nothing else in the tree references IWIC*.
|
||||
target_link_libraries(libfastfetch
|
||||
PRIVATE "windowscodecs"
|
||||
)
|
||||
endif()
|
||||
if(NOT ENABLE_WIN81_COMPAT)
|
||||
target_link_libraries(libfastfetch
|
||||
PRIVATE "mincore"
|
||||
@@ -2021,14 +2115,35 @@ elseif(ANDROID)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Both APIs (AImageDecoder, AMediaCodec) are newer than the API level this build targets --
|
||||
# AImageDecoder is 30, and AMediaCodec_getName / AMediaCodec_releaseName are 28 -- while fastfetch
|
||||
# keeps running on devices below those levels. The NDK headers mark the entry points unavailable, so
|
||||
# they have to be weak references: otherwise the executable would fail to load on an older device.
|
||||
# Guarding every use with __builtin_available() is what makes that safe, and turning the guard
|
||||
# diagnostic into an error is what keeps a use from slipping past it.
|
||||
# Three APIs are newer than the API level this build targets: AImageDecoder is 30,
|
||||
# AndroidBitmap_compress is 30, and AMediaCodec_getName / AMediaCodec_releaseName are 28.
|
||||
# fastfetch keeps running on devices below those levels, and the NDK headers mark the entry
|
||||
# points unavailable there, so they have to be weak references -- a strong reference would fail
|
||||
# the load on an older device. Guarding every use with __builtin_available() is what makes that
|
||||
# safe, and turning the guard diagnostic into an error keeps a use from slipping past it.
|
||||
#
|
||||
# Each one lives in the library that provides it, and that library is linked above:
|
||||
# AImageDecoder_* and AndroidBitmap_compress are exported by libjnigraphics.so, and
|
||||
# AMediaCodec_* by libmediandk.so. Checked on a device (Android 16, API 36) with
|
||||
# `readelf --dyn-syms /system/lib64/libjnigraphics.so` and a dlopen/dlsym probe: every
|
||||
# AImageDecoder_* entry point resolves there and none of them resolves in libandroid.so, which
|
||||
# is why libandroid.so is *not* linked -- nothing fastfetch calls comes from it. Keep the
|
||||
# provider library linked for the reason the NDK requires it: an undefined weak symbol resolves
|
||||
# to NULL rather than failing the load, and FF_ANDROID_API_AT_LEAST() reads the platform level,
|
||||
# not the symbol, so it would send the call straight through a NULL pointer on every device new
|
||||
# enough to pass the guard.
|
||||
#
|
||||
# camera_android.c is deliberately not in this list. The camera2 NDK is a *library* that starts at
|
||||
# API 24, and a weak reference weakens the symbol, never the library: DT_NEEDED would still name
|
||||
# libcamera2ndk.so, and a device below API 24 would fail to load fastfetch at all. The NDK's guide
|
||||
# is explicit that there is no such thing as a weak library reference, and that dlopen is the only
|
||||
# way to reach a library newer than minSdkVersion. Nothing here pins ANDROID_PLATFORM, so a build
|
||||
# aimed below 24 would need that; on the supported path -- Termux, which targets 24 -- the library
|
||||
# is always present and the plain call is correct. libjnigraphics itself is one of them and is
|
||||
# linked unconditionally above, so the encoder only needs its symbol weakened, not its library.
|
||||
set_source_files_properties(
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/logo/image/aid.c
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/detection/media/media_android.c
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/detection/codec/codec_android.c
|
||||
PROPERTIES COMPILE_OPTIONS "-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__;-Werror=unguarded-availability")
|
||||
elseif(Haiku)
|
||||
@@ -2091,7 +2206,17 @@ if(NOT WIN32)
|
||||
endif()
|
||||
endif()
|
||||
if(NOT ENABLE_WORDEXP)
|
||||
message(STATUS "wordexp.h not found or disabled, glob used instead")
|
||||
# wordexp.h is not the only thing that can be missing. bionic declares glob() and globfree()
|
||||
# only from API 28, so an NDK build aimed at 24 has neither and a fallback that assumed glob
|
||||
# would have nothing to call. The symbol is what has to be checked, not the header: <glob.h>
|
||||
# is present at every level, it just declares nothing below 28.
|
||||
check_symbol_exists("glob" "glob.h" HAVE_GLOB)
|
||||
if(HAVE_GLOB)
|
||||
target_compile_definitions(libfastfetch PUBLIC FF_HAVE_GLOB=1)
|
||||
message(STATUS "wordexp.h not found or disabled, glob used instead")
|
||||
else()
|
||||
message(STATUS "neither wordexp.h nor glob.h is usable, config paths will not be expanded")
|
||||
endif()
|
||||
endif()
|
||||
if(ENABLE_THREADS AND CMAKE_USE_PTHREADS_INIT)
|
||||
CHECK_INCLUDE_FILE("pthread_np.h" HAVE_PTHREAD_NP)
|
||||
@@ -2150,6 +2275,15 @@ set(CMAKE_CXX_IMPLICIT_LINK_DIRECTORIES "")
|
||||
set_target_properties(fastfetch PROPERTIES LINKER_LANGUAGE C)
|
||||
|
||||
if(WIN32)
|
||||
# version.rc embeds manifest.xml (RT_MANIFEST) and logo.ico (ICON) as *resource
|
||||
# references*, not as #includes, so the resource compiler never reports them and
|
||||
# CMake would keep a stale .res when only the manifest or the icon changes. Both
|
||||
# are plain files rather than generated ones, so declare them by hand; the source
|
||||
# file property is per-path, which is why flashfetch below needs no repeat.
|
||||
set_property(SOURCE src/common/windows/version.rc APPEND PROPERTY OBJECT_DEPENDS
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/common/windows/manifest.xml"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/common/windows/logo.ico"
|
||||
)
|
||||
target_sources(fastfetch
|
||||
PRIVATE src/common/windows/version.rc
|
||||
)
|
||||
@@ -2157,6 +2291,15 @@ elseif(APPLE)
|
||||
target_link_options(fastfetch
|
||||
PRIVATE LINKER:-sectcreate,__TEXT,__info_plist,Info.plist
|
||||
)
|
||||
# `LINKER:` options are handed to ld verbatim and are not registered as build
|
||||
# dependencies, so without this ninja never relinks when Info.plist changes.
|
||||
set_property(TARGET fastfetch APPEND PROPERTY LINK_DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/Info.plist")
|
||||
# ld derives the ad-hoc code-signing identifier from the output file name and
|
||||
# ignores the embedded CFBundleIdentifier, so set it explicitly.
|
||||
add_custom_command(TARGET fastfetch POST_BUILD
|
||||
COMMAND codesign --force --sign - --identifier "${FASTFETCH_BUNDLE_ID}" "$<TARGET_FILE:fastfetch>"
|
||||
VERBATIM
|
||||
)
|
||||
endif()
|
||||
|
||||
if(BINARY_LINK_TYPE STREQUAL "static")
|
||||
@@ -2182,8 +2325,15 @@ if (BUILD_FLASHFETCH)
|
||||
PRIVATE src/common/windows/version.rc
|
||||
)
|
||||
elseif(APPLE)
|
||||
# flashfetch embeds its own plist so that its CFBundleIdentifier matches the
|
||||
# identifier it is signed with below (see the APPLE block near the top).
|
||||
target_link_options(flashfetch
|
||||
PRIVATE LINKER:-sectcreate,__TEXT,__info_plist,Info.plist
|
||||
PRIVATE LINKER:-sectcreate,__TEXT,__info_plist,Info.plist.flashfetch
|
||||
)
|
||||
set_property(TARGET flashfetch APPEND PROPERTY LINK_DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/Info.plist.flashfetch")
|
||||
add_custom_command(TARGET flashfetch POST_BUILD
|
||||
COMMAND codesign --force --sign - --identifier "${FLASHFETCH_BUNDLE_ID}" "$<TARGET_FILE:flashfetch>"
|
||||
VERBATIM
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -2197,6 +2347,20 @@ endif()
|
||||
###################
|
||||
|
||||
if (BUILD_TESTS)
|
||||
add_executable(fastfetch-test-cache
|
||||
tests/cache.c
|
||||
)
|
||||
target_link_libraries(fastfetch-test-cache
|
||||
PRIVATE libfastfetch
|
||||
)
|
||||
|
||||
add_executable(fastfetch-test-logo
|
||||
tests/logo.c
|
||||
)
|
||||
target_link_libraries(fastfetch-test-logo
|
||||
PRIVATE libfastfetch
|
||||
)
|
||||
|
||||
add_executable(fastfetch-test-strbuf
|
||||
tests/strbuf.c
|
||||
)
|
||||
@@ -2309,7 +2473,16 @@ if (BUILD_TESTS)
|
||||
PRIVATE libfastfetch
|
||||
)
|
||||
|
||||
add_executable(fastfetch-test-gpu-pci
|
||||
tests/gpu_pci.c
|
||||
)
|
||||
target_link_libraries(fastfetch-test-gpu-pci
|
||||
PRIVATE libfastfetch
|
||||
)
|
||||
|
||||
enable_testing()
|
||||
add_test(NAME test-cache COMMAND fastfetch-test-cache)
|
||||
add_test(NAME test-logo COMMAND fastfetch-test-logo)
|
||||
add_test(NAME test-strbuf COMMAND fastfetch-test-strbuf)
|
||||
add_test(NAME test-list COMMAND fastfetch-test-list)
|
||||
add_test(NAME test-format COMMAND fastfetch-test-format)
|
||||
@@ -2326,6 +2499,7 @@ if (BUILD_TESTS)
|
||||
add_test(NAME test-temps COMMAND fastfetch-test-temps)
|
||||
add_test(NAME test-endian COMMAND fastfetch-test-endian)
|
||||
add_test(NAME test-endian-bigendian COMMAND fastfetch-test-endian-bigendian)
|
||||
add_test(NAME test-gpu-pci COMMAND fastfetch-test-gpu-pci)
|
||||
endif()
|
||||
|
||||
##################
|
||||
@@ -2387,7 +2561,12 @@ install(
|
||||
|
||||
set(CPACK_GENERATOR "TGZ;ZIP")
|
||||
if(APPLE)
|
||||
string(TOLOWER "${CMAKE_PROJECT_NAME}-macos-${CMAKE_SYSTEM_PROCESSOR}" CPACK_PACKAGE_FILE_NAME) # use macos instead of darwin
|
||||
if(CMAKE_OSX_ARCHITECTURES MATCHES ";") # multiple architectures, e.g. arm64;x86_64
|
||||
set(_FF_CPACK_ARCH universal)
|
||||
else()
|
||||
set(_FF_CPACK_ARCH ${CMAKE_SYSTEM_PROCESSOR})
|
||||
endif()
|
||||
string(TOLOWER "${CMAKE_PROJECT_NAME}-macos-${_FF_CPACK_ARCH}" CPACK_PACKAGE_FILE_NAME) # use macos instead of darwin
|
||||
elseif(IS_MUSL)
|
||||
string(TOLOWER "${CMAKE_PROJECT_NAME}-musl-${CMAKE_SYSTEM_PROCESSOR}" CPACK_PACKAGE_FILE_NAME)
|
||||
else()
|
||||
|
||||
+2
-7
@@ -96,7 +96,7 @@ cmake -DBUILD_TESTS=On -DENABLE_VULKAN=OFF -DENABLE_WAYLAND=OFF -DENABLE_X11=OFF
|
||||
|
||||
Required: CMake ≥ 3.21 and a C23 compiler (GCC, Clang or MSVC). Everything else is optional.
|
||||
|
||||
Optional dependencies are auto-detected: libpci, libdrm, vulkan, wayland, xcb, xrandr, dbus, sqlite3, rpm, imagemagick{6,7}, chafa, zlib, egl, glx, opencl, freetype, pulse, ddcutil, elf, libzfs, and more.
|
||||
Optional dependencies are auto-detected: libpci, libdrm, vulkan, wayland, xcb, xrandr, dbus, sqlite3, rpm, imagemagick{6,7}, chafa, zlib, egl, glx, opencl, freetype, elf, libzfs, and more.
|
||||
|
||||
---
|
||||
|
||||
@@ -587,10 +587,6 @@ clang-format -i src/modules/foo/*.c src/modules/foo/*.h
|
||||
| Module options | `FF<Name>Options` | `FFCPUOptions` |
|
||||
| Detection results | `FF<Name>Result` | `FFCPUResult` |
|
||||
|
||||
### Spelling
|
||||
|
||||
CI runs codespell (`.codespellrc`). Known false positives are listed in `ignore-words-list` (`iterm`, `compiletime`, and various non-English distro words). Add new words there rather than changing the code.
|
||||
|
||||
### Compiler warnings
|
||||
|
||||
The build enables `-Wall -Wextra -Wconversion` plus several `-Werror`s:
|
||||
@@ -684,7 +680,7 @@ cmake --build build
|
||||
cd build && ctest --output-on-failure
|
||||
```
|
||||
|
||||
Coverage focuses on the core data structures and the formatting engine in `common/`. The `detection/` layer has no automated tests, because it depends on the state of a running system; it is instead covered by the CI matrix — 20 workflows under `.github/workflows/` spanning Linux (including musl, loong64, armv7l, i686), macOS, Windows, FreeBSD, NetBSD, OpenBSD, DragonFly, Solaris, OmniOS and Haiku, plus spellcheck and benchmark jobs.
|
||||
Coverage focuses on the core data structures and the formatting engine in `common/`. The `detection/` layer has no automated tests, because it depends on the state of a running system; it is instead covered by the CI matrix — 20 workflows under `.github/workflows/` spanning Linux (including musl, loong64, armv7l, i686), Android, macOS, Windows, FreeBSD, NetBSD, OpenBSD, DragonFly, Solaris, OmniOS and Haiku, plus a benchmark job. The Android job is the one that does not run what it builds: it cross-compiles for arm64-v8a, armeabi-v7a and x86_64 with the NDK at API 24 and inspects the result instead, because CI has no device or emulator. Its dynamic-symbol checks are what keep the weak references the platform branch depends on from turning into load failures on older devices.
|
||||
|
||||
**If you modify `common/FFstrbuf.h`, `common/format.h` or `common/color.h`, extend the corresponding test.**
|
||||
|
||||
@@ -707,7 +703,6 @@ Coverage focuses on the core data structures and the formatting engine in `commo
|
||||
|
||||
```sh
|
||||
clang-format -i <changed files> # format
|
||||
codespell # spelling
|
||||
cmake -B build -DBUILD_TESTS=On && cmake --build build -j
|
||||
cd build && ctest --output-on-failure # tests
|
||||
./build/fastfetch --format json # verify JSON output is well-formed
|
||||
|
||||
@@ -248,7 +248,20 @@ Set the key to a white space.
|
||||
|
||||
### Q: How can I display images on Windows?
|
||||
|
||||
As of April 2025:
|
||||
#### Windows Terminal
|
||||
|
||||
Windows Terminal supports the sixel image protocol only.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"logo": {
|
||||
"type": "sixel", // DO NOT USE "auto"
|
||||
"source": "C:/path/to/image.png", // ~ can be used to refer to the Windows user home directory
|
||||
"width": <image-width-in-chars>, // Optional
|
||||
"height": <image-height-in-chars> // Optional
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### mintty and Wezterm
|
||||
|
||||
@@ -260,38 +273,7 @@ In `config.jsonc`:
|
||||
"logo": {
|
||||
"type": "iterm",
|
||||
"source": "C:/path/to/image.png",
|
||||
"width": <num-in-chars>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Windows Terminal
|
||||
|
||||
Windows Terminal supports the sixel image protocol only.
|
||||
|
||||
* If you installed fastfetch through MSYS2:
|
||||
1. Install imagemagick: `pacman -S mingw-w64-<subsystem>-x86_64-imagemagick`
|
||||
2. In `config.jsonc`:
|
||||
```jsonc
|
||||
{
|
||||
"logo": {
|
||||
"type": "sixel", // DO NOT USE "auto"
|
||||
"source": "C:/path/to/image.png", // Do NOT use `~` as fastfetch is a native Windows program and doesn't apply cygwin path conversion
|
||||
"width": <image-width-in-chars>, // Optional
|
||||
"height": <image-height-in-chars> // Optional
|
||||
}
|
||||
}
|
||||
```
|
||||
* If you installed fastfetch via scoop or downloaded the binary directly from the GitHub Releases page:
|
||||
1. Convert your image manually to sixel format using [any online image conversion service](https://www.google.com/search?q=convert+image+to+sixel)
|
||||
2. In `config.jsonc`:
|
||||
```jsonc
|
||||
{
|
||||
"logo": {
|
||||
"type": "raw", // DO NOT USE "auto"
|
||||
"source": "C:/path/to/image.sixel",
|
||||
"width": <image-width-in-chars>, // Required
|
||||
"height": <image-height-in-chars> // Required
|
||||
"width": <image-width-in-chars>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -33,7 +33,9 @@ _fastfetch() {
|
||||
if type _filedir &>/dev/null; then
|
||||
_filedir
|
||||
elif type compgen &>/dev/null; then
|
||||
COMPREPLY+=($(compgen -f -- "$cur"))
|
||||
local -a files
|
||||
readarray -t files < <(compgen -f -- "$cur")
|
||||
COMPREPLY+=("${files[@]}")
|
||||
fi
|
||||
return
|
||||
;;
|
||||
|
||||
Vendored
+1
-1
@@ -2,7 +2,7 @@ Source: fastfetch
|
||||
Section: universe/utils
|
||||
Priority: optional
|
||||
Maintainer: Carter Li <zhangsongcui@live.cn>
|
||||
Build-Depends: libelf-dev, libvulkan-dev, libwayland-dev, libxrandr-dev, libxcb-randr0-dev, libdconf-dev, libdbus-1-dev, libmagickcore-dev, libsqlite3-dev, librpm-dev, libegl-dev, libglx-dev, ocl-icd-opencl-dev, libpulse-dev, libdrm-dev, libddcutil-dev, libchafa-dev, libefl-all-dev, pkgconf, cmake (>= 3.12), debhelper (>= 11.2), dh-cmake, dh-cmake-compat (= 1), dh-sequence-cmake, dh-sequence-ctest, ninja-build, python3-setuptools
|
||||
Build-Depends: libelf-dev, libvulkan-dev, libwayland-dev, libxrandr-dev, libxcb-randr0-dev, libdconf-dev, libdbus-1-dev, libmagickcore-dev, libsqlite3-dev, librpm-dev, libegl-dev, libglx-dev, ocl-icd-opencl-dev, libdrm-dev, libchafa-dev, libefl-all-dev, pkgconf, cmake (>= 3.12), debhelper (>= 11.2), dh-cmake, dh-cmake-compat (= 1), dh-sequence-cmake, dh-sequence-ctest, ninja-build, python3-setuptools
|
||||
Standards-Version: 4.0.0
|
||||
Homepage: https://github.com/fastfetch-cli/fastfetch
|
||||
|
||||
|
||||
@@ -195,6 +195,7 @@
|
||||
"file-raw": "Text file, printed as is",
|
||||
"data": "Text data, printed with color code replacement",
|
||||
"data-raw": "Text data, printed as is",
|
||||
"command-raw": "Command to run, whose output is printed as is",
|
||||
"sixel": "Image file, rendered as sixel",
|
||||
"kitty": "Image file, rendered using the Kitty graphics protocol",
|
||||
"kitty-direct": "Image file, tells the terminal emulator to read image data from the specified file",
|
||||
@@ -269,6 +270,14 @@
|
||||
"type": "num"
|
||||
}
|
||||
},
|
||||
{
|
||||
"long": "logo-padding-bottom",
|
||||
"desc": "Set the padding at the bottom of the logo",
|
||||
"remark": "Only used with \"--logo-position top\"",
|
||||
"arg": {
|
||||
"type": "num"
|
||||
}
|
||||
},
|
||||
{
|
||||
"long": "logo-print-remaining",
|
||||
"desc": "Specify whether to print remaining logo lines when the logo is taller than the module list",
|
||||
@@ -284,6 +293,7 @@
|
||||
"arg": {
|
||||
"type": "enum",
|
||||
"enum": {
|
||||
"auto": "Automatically choose left or top based on terminal width",
|
||||
"left": "Left",
|
||||
"top": "Top",
|
||||
"right": "Right"
|
||||
@@ -620,6 +630,7 @@
|
||||
"cs": "Czech",
|
||||
"de": "German",
|
||||
"es": "Spanish",
|
||||
"fi": "Finnish",
|
||||
"fr": "French",
|
||||
"gl": "Galician",
|
||||
"he": "Hebrew",
|
||||
|
||||
+115
-59
@@ -19,15 +19,14 @@
|
||||
},
|
||||
"key": {
|
||||
"description": "Module key\nUse a single space (` `) to hide the key",
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
"type": "string"
|
||||
},
|
||||
"keyColor": {
|
||||
"description": "Color of the module key. Overrides `display.color.key`",
|
||||
"description": "Color of the module key. Overrides `display.color.keys`",
|
||||
"$ref": "#/$defs/colors"
|
||||
},
|
||||
"keyWidth": {
|
||||
"description": "Width of the module key. Overrides `display.keyWidth`",
|
||||
"description": "Width of the module key. Overrides `display.key.width`",
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
@@ -131,7 +130,9 @@
|
||||
"SunOS",
|
||||
"OpenBSD",
|
||||
"NetBSD",
|
||||
"Haiku"
|
||||
"Haiku",
|
||||
"GNU",
|
||||
"Unknown"
|
||||
]
|
||||
},
|
||||
"architectures": {
|
||||
@@ -150,7 +151,9 @@
|
||||
"sparc",
|
||||
"alpha",
|
||||
"hppa",
|
||||
"m68k"
|
||||
"sh",
|
||||
"m68k",
|
||||
"Unknown"
|
||||
]
|
||||
},
|
||||
"conditions": {
|
||||
@@ -274,19 +277,19 @@
|
||||
|
||||
|
||||
"batteryFormat": {
|
||||
"description": "Output format for the `Battery` module. See Wiki for formatting syntax\n 1. {manufacturer}: Battery manufacturer\n 2. {model-name}: Battery model name\n 3. {technology}: Battery technology\n 4. {capacity}: Battery capacity (percentage num)\n 5. {status}: Battery status\n 6. {temperature}: Battery temperature (formatted)\n 7. {cycle-count}: Battery cycle count\n 8. {serial}: Battery serial number\n 9. {manufacture-date}: Battery manufacture date\n 10. {capacity-bar}: Battery capacity (percentage bar)\n 11. {time-days}: Battery time remaining days\n 12. {time-hours}: Battery time remaining hours\n 13. {time-minutes}: Battery time remaining minutes\n 14. {time-seconds}: Battery time remaining seconds\n 15. {time-formatted}: Battery time remaining (formatted)",
|
||||
"description": "Output format for the `Battery` module. See Wiki for formatting syntax\n 1. {manufacturer}: Battery manufacturer\n 2. {model-name}: Battery model name *\n 3. {technology}: Battery technology\n 4. {capacity}: Battery capacity (percentage num)\n 5. {status}: Battery status\n 6. {temperature}: Battery temperature (formatted)\n 7. {cycle-count}: Battery cycle count\n 8. {serial}: Battery serial number\n 9. {manufacture-date}: Battery manufacture date\n 10. {capacity-bar}: Battery capacity (percentage bar)\n 11. {time-days}: Battery time remaining days\n 12. {time-hours}: Battery time remaining hours\n 13. {time-minutes}: Battery time remaining minutes\n 14. {time-seconds}: Battery time remaining seconds\n 15. {time-formatted}: Battery time remaining (formatted)",
|
||||
"type": "string"
|
||||
},
|
||||
"biosFormat": {
|
||||
"description": "Output format for the `BIOS` module. See Wiki for formatting syntax\n 1. {date}: BIOS date\n 2. {release}: BIOS release\n 3. {vendor}: BIOS vendor\n 4. {version}: BIOS version\n 5. {type}: Firmware type",
|
||||
"description": "Output format for the `BIOS` module. See Wiki for formatting syntax\n 1. {date}: BIOS date\n 2. {release}: BIOS release\n 3. {vendor}: BIOS vendor\n 4. {version}: BIOS version\n 5. {type}: Firmware type *",
|
||||
"type": "string"
|
||||
},
|
||||
"bluetoothFormat": {
|
||||
"description": "Output format for the `Bluetooth` module. See Wiki for formatting syntax\n 1. {name}: Name\n 2. {address}: Address\n 3. {type}: Type\n 4. {battery-percentage}: Battery percentage number\n 5. {connected}: Is connected\n 6. {battery-percentage-bar}: Battery percentage bar",
|
||||
"description": "Output format for the `Bluetooth` module. See Wiki for formatting syntax\n 1. {name}: Name\n 2. {address}: Address\n 3. {type}: Type\n 4. {device-type}: Bluetooth stacks the device answers on\n 5. {battery-percentage}: Battery percentage number\n 6. {battery-percentage-bar}: Battery percentage bar\n 7. {signal-quality}: Signal quality number\n 8. {signal-quality-bar}: Signal quality bar\n 9. {connected}: Is connected",
|
||||
"type": "string"
|
||||
},
|
||||
"bluetoothradioFormat": {
|
||||
"description": "Output format for the `BluetoothRadio` module. See Wiki for formatting syntax\n 1. {name}: Radio name for discovering\n 2. {address}: Address\n 3. {lmp-version}: LMP version\n 4. {lmp-subversion}: LMP subversion\n 5. {version}: Bluetooth version\n 6. {vendor}: Vendor\n 7. {discoverable}: Discoverable\n 8. {connectable}: Connectable / Pairable",
|
||||
"description": "Output format for the `BluetoothRadio` module. See Wiki for formatting syntax\n 1. {name}: Radio name for discovering *\n 2. {address}: Address\n 3. {lmp-version}: LMP version\n 4. {lmp-subversion}: LMP subversion\n 5. {version}: Bluetooth version\n 6. {vendor}: Vendor\n 7. {discoverable}: Discoverable\n 8. {connectable}: Connectable / Pairable",
|
||||
"type": "string"
|
||||
},
|
||||
"boardFormat": {
|
||||
@@ -298,11 +301,11 @@
|
||||
"type": "string"
|
||||
},
|
||||
"brightnessFormat": {
|
||||
"description": "Output format for the `Brightness` module. See Wiki for formatting syntax\n 1. {percentage}: Screen brightness (percentage num)\n 2. {name}: Screen name\n 3. {max}: Maximum brightness value\n 4. {min}: Minimum brightness value\n 5. {current}: Current brightness value\n 6. {percentage-bar}: Screen brightness (percentage bar)\n 7. {is-builtin}: Is built-in screen",
|
||||
"description": "Output format for the `Brightness` module. See Wiki for formatting syntax\n 1. {percentage}: Screen brightness (percentage num)\n 2. {name}: Screen name *\n 3. {max}: Maximum brightness value\n 4. {min}: Minimum brightness value\n 5. {current}: Current brightness value\n 6. {percentage-bar}: Screen brightness (percentage bar)\n 7. {is-builtin}: Is built-in screen",
|
||||
"type": "string"
|
||||
},
|
||||
"btrfsFormat": {
|
||||
"description": "Output format for the `Btrfs` module. See Wiki for formatting syntax\n 1. {name}: Name / Label\n 2. {uuid}: UUID\n 3. {devices}: Associated devices\n 4. {features}: Enabled features\n 5. {used}: Size used\n 6. {allocated}: Size allocated\n 7. {total}: Size total\n 8. {used-percentage}: Used percentage num\n 9. {allocated-percentage}: Allocated percentage num\n 10. {used-percentage-bar}: Used percentage bar\n 11. {allocated-percentage-bar}: Allocated percentage bar\n 12. {node-size}: Node size\n 13. {sector-size}: Sector size",
|
||||
"description": "Output format for the `Btrfs` module. See Wiki for formatting syntax\n 1. {name}: Name / Label *\n 2. {uuid}: UUID\n 3. {devices}: Associated devices\n 4. {features}: Enabled features\n 5. {used}: Size used\n 6. {allocated}: Size allocated\n 7. {total}: Size total\n 8. {used-percentage}: Used percentage num\n 9. {allocated-percentage}: Allocated percentage num\n 10. {used-percentage-bar}: Used percentage bar\n 11. {allocated-percentage-bar}: Allocated percentage bar\n 12. {node-size}: Node size\n 13. {sector-size}: Sector size",
|
||||
"type": "string"
|
||||
},
|
||||
"cameraFormat": {
|
||||
@@ -314,7 +317,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"codecFormat": {
|
||||
"description": "Output format for the `Codec` module. See Wiki for formatting syntax\n 1. {gpu}: GPU name\n 2. {direction}: Decoder / Encoder\n 3. {types}: Compatibility alias of codec types\n 4. {platform-api}: Platform API used for detection",
|
||||
"description": "Output format for the `Codec` module. See Wiki for formatting syntax\n 1. {gpu}: GPU name *\n 2. {direction}: Decoder / Encoder *\n 3. {types}: Compatibility alias of codec types\n 4. {platform-api}: Platform API used for detection",
|
||||
"type": "string"
|
||||
},
|
||||
"commandFormat": {
|
||||
@@ -322,11 +325,11 @@
|
||||
"type": "string"
|
||||
},
|
||||
"cpuFormat": {
|
||||
"description": "Output format for the `CPU` module. See Wiki for formatting syntax\n 1. {name}: Name\n 2. {vendor}: Vendor\n 3. {cores-physical}: Physical core count\n 4. {cores-logical}: Logical core count\n 5. {cores-online}: Online core count\n 6. {freq-base}: Base frequency (formatted)\n 7. {freq-max}: Max frequency (formatted)\n 8. {temperature}: Temperature (formatted)\n 9. {core-types}: Logical core count grouped by frequency\n 10. {packages}: Processor package count\n 11. {march}: CPU microarchitecture\n 12. {numa-nodes}: NUMA node count\n 13. {code-name}: CPU code name\n 14. {technology}: CPU technology",
|
||||
"description": "Output format for the `CPU` module. See Wiki for formatting syntax\n 1. {name}: Name\n 2. {vendor}: Vendor\n 3. {cores-physical}: Physical core count\n 4. {cores-logical}: Logical core count\n 5. {cores-online}: Online core count\n 6. {freq-base}: Base frequency (formatted)\n 7. {freq-max}: Max frequency (formatted)\n 8. {temperature}: Temperature (formatted)\n 9. {core-types}: Logical core count grouped by frequency\n 10. {packages}: Processor package count\n 11. {march}: CPU microarchitecture\n 12. {numa-nodes}: NUMA node count",
|
||||
"type": "string"
|
||||
},
|
||||
"cpucacheFormat": {
|
||||
"description": "Output format for the `CPUCache` module. See Wiki for formatting syntax\n 1. {result}: Separate result\n 2. {sum}: Sum result",
|
||||
"description": "Output format for the `CPUCache` module. See Wiki for formatting syntax\n 1. {result}: Separate result\n 2. {sum}: Sum result\n 3. {level}: Cache level *",
|
||||
"type": "string"
|
||||
},
|
||||
"cpuusageFormat": {
|
||||
@@ -346,15 +349,15 @@
|
||||
"type": "string"
|
||||
},
|
||||
"displayFormat": {
|
||||
"description": "Output format for the `Display` module. See Wiki for formatting syntax\n 1. {width}: Screen configured width (in pixels)\n 2. {height}: Screen configured height (in pixels)\n 3. {refresh-rate}: Screen configured refresh rate (in Hz)\n 4. {scaled-width}: Screen scaled width (in pixels)\n 5. {scaled-height}: Screen scaled height (in pixels)\n 6. {name}: Screen name\n 7. {type}: Screen type (Built-in or External)\n 8. {rotation}: Screen rotation (in degrees)\n 9. {is-primary}: True if being the primary screen\n 10. {physical-width}: Screen physical width (in millimeters)\n 11. {physical-height}: Screen physical height (in millimeters)\n 12. {inch}: Physical diagonal length in inches\n 13. {ppi}: Pixels per inch (PPI)\n 14. {bit-depth}: Bits per color channel\n 15. {hdr-enabled}: True if high dynamic range (HDR) mode is enabled\n 16. {manufacture-year}: Year of manufacturing\n 17. {manufacture-week}: Nth week of manufacturing in the year\n 18. {serial}: Serial number\n 19. {platform-api}: The platform API used when detecting the display\n 20. {hdr-compatible}: True if the display is HDR compatible\n 21. {scale-factor}: HiDPI scale factor\n 22. {preferred-width}: Screen preferred width (in pixels)\n 23. {preferred-height}: Screen preferred height (in pixels)\n 24. {preferred-refresh-rate}: Screen preferred refresh rate (in Hz)\n 25. {dpi}: DPI",
|
||||
"description": "Output format for the `Display` module. See Wiki for formatting syntax\n 1. {width}: Screen configured width (in pixels)\n 2. {height}: Screen configured height (in pixels)\n 3. {refresh-rate}: Screen configured refresh rate (in Hz)\n 4. {scaled-width}: Screen scaled width (in pixels)\n 5. {scaled-height}: Screen scaled height (in pixels)\n 6. {name}: Screen name *\n 7. {type}: Screen type (Built-in or External) *\n 8. {rotation}: Screen rotation (in degrees)\n 9. {is-primary}: True if being the primary screen\n 10. {physical-width}: Screen physical width (in millimeters)\n 11. {physical-height}: Screen physical height (in millimeters)\n 12. {inch}: Physical diagonal length in inches\n 13. {ppi}: Pixels per inch (PPI)\n 14. {bit-depth}: Bits per color channel\n 15. {hdr-enabled}: True if high dynamic range (HDR) mode is enabled\n 16. {manufacture-year}: Year of manufacturing\n 17. {manufacture-week}: Nth week of manufacturing in the year\n 18. {serial}: Serial number\n 19. {platform-api}: The platform API used when detecting the display\n 20. {hdr-compatible}: True if the display is HDR compatible\n 21. {scale-factor}: HiDPI scale factor\n 22. {preferred-width}: Screen preferred width (in pixels)\n 23. {preferred-height}: Screen preferred height (in pixels)\n 24. {preferred-refresh-rate}: Screen preferred refresh rate (in Hz)\n 25. {dpi}: DPI",
|
||||
"type": "string"
|
||||
},
|
||||
"diskFormat": {
|
||||
"description": "Output format for the `Disk` module. See Wiki for formatting syntax\n 1. {size-used}: Size used\n 2. {size-total}: Size total\n 3. {size-percentage}: Size percentage num\n 4. {files-used}: Files used\n 5. {files-total}: Files total\n 6. {files-percentage}: Files percentage num\n 7. {is-external}: True if external volume\n 8. {is-hidden}: True if hidden volume\n 9. {filesystem}: Filesystem\n 10. {name}: Label / name\n 11. {is-readonly}: True if read-only\n 12. {create-time}: Create time in local timezone\n 13. {size-percentage-bar}: Size percentage bar\n 14. {files-percentage-bar}: Files percentage bar\n 15. {days}: Days after creation\n 16. {hours}: Hours after creation\n 17. {minutes}: Minutes after creation\n 18. {seconds}: Seconds after creation\n 19. {milliseconds}: Milliseconds after creation\n 20. {mountpoint}: Mount point / drive letter\n 21. {mount-from}: Mount from (device path)\n 22. {years}: Years integer after creation\n 23. {days-of-year}: Days of year after creation\n 24. {years-fraction}: Years fraction after creation\n 25. {size-free}: Size free\n 26. {size-available}: Size available",
|
||||
"description": "Output format for the `Disk` module. See Wiki for formatting syntax\n 1. {size-used}: Size used (formatted)\n 2. {size-total}: Size total (formatted)\n 3. {size-percentage}: Size percentage num\n 4. {files-used}: Files used\n 5. {files-total}: Files total\n 6. {files-percentage}: Files percentage num\n 7. {is-external}: True if external volume\n 8. {is-hidden}: True if hidden volume\n 9. {filesystem}: Filesystem *\n 10. {name}: Label / name *\n 11. {is-readonly}: True if read-only\n 12. {create-time}: Create time in local timezone\n 13. {size-percentage-bar}: Size percentage bar\n 14. {files-percentage-bar}: Files percentage bar\n 15. {days}: Days after creation\n 16. {hours}: Hours after creation\n 17. {minutes}: Minutes after creation\n 18. {seconds}: Seconds after creation\n 19. {milliseconds}: Milliseconds after creation\n 20. {mountpoint}: Mount point / drive letter *\n 21. {mount-from}: Mount from (device path) *\n 22. {years}: Years integer after creation\n 23. {days-of-year}: Days of year after creation\n 24. {years-fraction}: Years fraction after creation\n 25. {size-free}: Size free (formatted)\n 26. {bytes-used}: Size used (in bytes)\n 27. {bytes-total}: Size total (in bytes)",
|
||||
"type": "string"
|
||||
},
|
||||
"diskioFormat": {
|
||||
"description": "Output format for the `DiskIO` module. See Wiki for formatting syntax\n 1. {size-read}: Size of data read [per second] (formatted)\n 2. {size-written}: Size of data written [per second] (formatted)\n 3. {name}: Device name\n 4. {dev-path}: Device raw file path\n 5. {bytes-read}: Size of data read [per second] (in bytes)\n 6. {bytes-written}: Size of data written [per second] (in bytes)\n 7. {read-count}: Number of reads\n 8. {write-count}: Number of writes",
|
||||
"description": "Output format for the `DiskIO` module. See Wiki for formatting syntax\n 1. {size-read}: Size of data read [per second] (formatted)\n 2. {size-written}: Size of data written [per second] (formatted)\n 3. {name}: Device name *\n 4. {dev-path}: Device raw file path *\n 5. {bytes-read}: Size of data read [per second] (in bytes)\n 6. {bytes-written}: Size of data written [per second] (in bytes)\n 7. {read-count}: Number of reads\n 8. {write-count}: Number of writes",
|
||||
"type": "string"
|
||||
},
|
||||
"dnsFormat": {
|
||||
@@ -390,7 +393,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"kernelFormat": {
|
||||
"description": "Output format for the `Kernel` module. See Wiki for formatting syntax\n 1. {sysname}: Sysname\n 2. {release}: Release\n 3. {version}: Version\n 4. {arch}: Architecture\n 5. {display-version}: Display version\n 6. {page-size}: Page size",
|
||||
"description": "Output format for the `Kernel` module. See Wiki for formatting syntax\n 1. {sysname}: Sysname\n 2. {release}: Release\n 3. {version}: Version\n 4. {arch}: Architecture\n 5. {page-size}: Page size",
|
||||
"type": "string"
|
||||
},
|
||||
"keyboardFormat": {
|
||||
@@ -410,7 +413,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"localipFormat": {
|
||||
"description": "Output format for the `LocalIp` module. See Wiki for formatting syntax\n 1. {ipv4}: IPv4 address\n 2. {ipv6}: IPv6 address\n 3. {mac}: MAC address\n 4. {ifname}: Interface name\n 5. {is-default-route}: Is default route\n 6. {mtu}: MTU size in bytes\n 7. {speed}: Link speed (formatted)\n 8. {flags}: Interface flags",
|
||||
"description": "Output format for the `LocalIp` module. See Wiki for formatting syntax\n 1. {ipv4}: IPv4 address\n 2. {ipv6}: IPv6 address\n 3. {mac}: MAC address *\n 4. {ifname}: Interface name *\n 5. {is-default-route}: Is default route\n 6. {mtu}: MTU size in bytes\n 7. {speed}: Link speed (formatted)\n 8. {flags}: Interface flags",
|
||||
"type": "string"
|
||||
},
|
||||
"mediaFormat": {
|
||||
@@ -422,7 +425,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"monitorFormat": {
|
||||
"description": "Output format for the `Monitor` module. See Wiki for formatting syntax\n 1. {name}: Display name\n 2. {width}: Native resolution width in pixels\n 3. {height}: Native resolution height in pixels\n 4. {physical-width}: Physical width in millimeters\n 5. {physical-height}: Physical height in millimeters\n 6. {inch}: Physical diagonal length in inches\n 7. {ppi}: Pixels per inch (PPI)\n 8. {manufacture-year}: Year of manufacturing\n 9. {manufacture-week}: Nth week of manufacturing in the year\n 10. {serial}: Serial number\n 11. {refresh-rate}: Maximum refresh rate in Hz\n 12. {hdr-compatible}: True if the display is HDR compatible",
|
||||
"description": "Output format for the `Monitor` module. See Wiki for formatting syntax\n 1. {name}: Display name *\n 2. {width}: Native resolution width in pixels\n 3. {height}: Native resolution height in pixels\n 4. {physical-width}: Physical width in millimeters\n 5. {physical-height}: Physical height in millimeters\n 6. {inch}: Physical diagonal length in inches\n 7. {ppi}: Pixels per inch (PPI)\n 8. {manufacture-year}: Year of manufacturing\n 9. {manufacture-week}: Nth week of manufacturing in the year\n 10. {serial}: Serial number\n 11. {refresh-rate}: Maximum refresh rate in Hz\n 12. {hdr-compatible}: True if the display is HDR compatible",
|
||||
"type": "string"
|
||||
},
|
||||
"mouseFormat": {
|
||||
@@ -430,7 +433,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"netioFormat": {
|
||||
"description": "Output format for the `NetIO` module. See Wiki for formatting syntax\n 1. {rx-size}: Size of data received [per second] (formatted)\n 2. {tx-size}: Size of data sent [per second] (formatted)\n 3. {ifname}: Interface name\n 4. {is-default-route}: Is default route\n 5. {rx-bytes}: Size of data received [per second] (in bytes)\n 6. {tx-bytes}: Size of data sent [per second] (in bytes)\n 7. {rx-packets}: Number of packets received [per second]\n 8. {tx-packets}: Number of packets sent [per second]\n 9. {rx-errors}: Number of errors received [per second]\n 10. {tx-errors}: Number of errors sent [per second]\n 11. {rx-drops}: Number of packets dropped when receiving [per second]\n 12. {tx-drops}: Number of packets dropped when sending [per second]",
|
||||
"description": "Output format for the `NetIO` module. See Wiki for formatting syntax\n 1. {rx-size}: Size of data received [per second] (formatted)\n 2. {tx-size}: Size of data sent [per second] (formatted)\n 3. {ifname}: Interface name *\n 4. {is-default-route}: Is default route\n 5. {rx-bytes}: Size of data received [per second] (in bytes)\n 6. {tx-bytes}: Size of data sent [per second] (in bytes)\n 7. {rx-packets}: Number of packets received [per second]\n 8. {tx-packets}: Number of packets sent [per second]\n 9. {rx-errors}: Number of errors received [per second]\n 10. {tx-errors}: Number of errors sent [per second]\n 11. {rx-drops}: Number of packets dropped when receiving [per second]\n 12. {tx-drops}: Number of packets dropped when sending [per second]",
|
||||
"type": "string"
|
||||
},
|
||||
"openclFormat": {
|
||||
@@ -442,15 +445,15 @@
|
||||
"type": "string"
|
||||
},
|
||||
"osFormat": {
|
||||
"description": "Output format for the `OS` module. See Wiki for formatting syntax\n 1. {sysname}: Name of the kernel\n 2. {name}: Name of the OS\n 3. {pretty-name}: Pretty name of the OS, if available\n 4. {id}: ID of the OS\n 5. {id-like}: ID like of the OS\n 6. {variant}: Variant of the OS\n 7. {variant-id}: Variant ID of the OS\n 8. {version}: Version of the OS\n 9. {version-id}: Version ID of the OS\n 10. {codename}: Version codename of the OS\n 11. {build-id}: Build ID of the OS\n 12. {arch}: Architecture of the OS",
|
||||
"description": "Output format for the `OS` module. See Wiki for formatting syntax\n 1. {sysname}: Name of the kernel *\n 2. {name}: Name of the OS *\n 3. {pretty-name}: Pretty name of the OS, if available\n 4. {id}: ID of the OS\n 5. {id-like}: ID like of the OS\n 6. {variant}: Variant of the OS\n 7. {variant-id}: Variant ID of the OS\n 8. {version}: Version of the OS\n 9. {version-id}: Version ID of the OS\n 10. {codename}: Version codename of the OS\n 11. {build-id}: Build ID of the OS\n 12. {arch}: Architecture of the OS\n 13. {kernel-release}: Release of the kernel",
|
||||
"type": "string"
|
||||
},
|
||||
"packagesFormat": {
|
||||
"description": "Output format for the `Packages` module. See Wiki for formatting syntax\n 1. {am-system}: Number of am-system packages\n 2. {am-user}: Number of am-user (aka appman) packages\n 3. {appimage}: Number of appimage packages\n 4. {apk}: Number of apk packages\n 5. {brew}: Number of brew packages\n 6. {brew-cask}: Number of brew-cask packages\n 7. {cards}: Number of cards packages\n 8. {choco}: Number of choco packages\n 9. {crux}: Number of crux packages\n 10. {dpkg}: Number of dpkg packages\n 11. {emerge}: Number of emerge packages\n 12. {eopkg}: Number of eopkg packages\n 13. {flatpak-system}: Number of flatpak-system app packages\n 14. {flatpak-user}: Number of flatpak-user app packages\n 15. {guix-home}: Number of guix-home packages\n 16. {guix-system}: Number of guix-system packages\n 17. {guix-user}: Number of guix-user packages\n 18. {hpkg-system}: Number of hpkg-system packages\n 19. {hpkg-user}: Number of hpkg-user packages\n 20. {install-release}: Number of install-release packages\n 21. {kiss}: Number of kiss packages\n 22. {linglong}: Number of linglong packages\n 23. {lpkg}: Number of lpkg packages\n 24. {lpkgbuild}: Number of lpkgbuild packages\n 25. {macports}: Number of macports packages\n 26. {mport}: Number of mport packages\n 27. {moss}: Number of moss packages\n 28. {nix-default}: Number of nix-default packages\n 29. {nix-system}: Number of nix-system packages\n 30. {nix-user}: Number of nix-user packages\n 31. {opkg}: Number of opkg packages\n 32. {pacman}: Number of pacman packages\n 33. {pacman-branch}: Pacman branch on manjaro\n 34. {pacstall}: Number of pacstall packages\n 35. {paludis}: Number of paludis packages\n 36. {pisi}: Number of pisi packages\n 37. {pkg}: Number of pkg packages\n 38. {pkgsrc}: Number of pkgsrc packages\n 39. {pkgtool}: Number of pkgtool packages\n 40. {porg}: Number of porg packages\n 41. {rpm}: Number of rpm packages\n 42. {scoop-global}: Number of scoop-global packages\n 43. {scoop-user}: Number of scoop-user packages\n 44. {snap}: Number of snap packages\n 45. {soar}: Number of soar packages\n 46. {sorcery}: Number of sorcery packages\n 47. {winget}: Number of winget packages\n 48. {xbps}: Number of xbps packages\n 49. {brew-all}: Total number of all brew packages\n 50. {flatpak-all}: Total number of all flatpak app packages\n 51. {guix-all}: Total number of all guix packages\n 52. {hpkg-all}: Total number of all hpkg packages\n 53. {nix-all}: Total number of all nix packages\n 54. {all}: Number of all packages",
|
||||
"description": "Output format for the `Packages` module. See Wiki for formatting syntax\n 1. {am-system}: Number of am-system packages\n 2. {am-user}: Number of am-user (aka appman) packages\n 3. {appimage}: Number of appimage packages\n 4. {apk}: Number of apk packages\n 5. {brew}: Number of brew packages\n 6. {brew-cask}: Number of brew-cask packages\n 7. {cards}: Number of cards packages\n 8. {choco}: Number of choco packages\n 9. {crux}: Number of crux packages\n 10. {dpkg}: Number of dpkg packages\n 11. {emerge}: Number of emerge packages\n 12. {eopkg}: Number of eopkg packages\n 13. {flatpak-system}: Number of flatpak-system app packages\n 14. {flatpak-user}: Number of flatpak-user app packages\n 15. {guix-home}: Number of guix-home packages\n 16. {guix-system}: Number of guix-system packages\n 17. {guix-user}: Number of guix-user packages\n 18. {hpkg-system}: Number of hpkg-system packages\n 19. {hpkg-user}: Number of hpkg-user packages\n 20. {install-release}: Number of install-release packages\n 21. {kiss}: Number of kiss packages\n 22. {linglong}: Number of linglong packages\n 23. {lpkg}: Number of lpkg packages\n 24. {lpkgbuild}: Number of lpkgbuild packages\n 25. {macports}: Number of macports packages\n 26. {mport}: Number of mport packages\n 27. {moss}: Number of moss packages\n 28. {nix-default}: Number of nix-default packages\n 29. {nix-system}: Number of nix-system packages\n 30. {nix-user}: Number of nix-user packages\n 31. {opkg}: Number of opkg packages\n 32. {pacman}: Number of pacman packages\n 33. {pacman-branch}: Pacman branch on manjaro\n 34. {pacstall}: Number of pacstall packages\n 35. {paludis}: Number of paludis packages\n 36. {pisi}: Number of pisi packages\n 37. {pkg}: Number of pkg packages\n 38. {pkgsrc}: Number of pkgsrc packages\n 39. {pkgtool}: Number of pkgtool packages\n 40. {porg}: Number of porg packages\n 41. {rpm}: Number of rpm packages\n 42. {rum}: Number of rum overlay packages\n 43. {scoop-global}: Number of scoop-global packages\n 44. {scoop-user}: Number of scoop-user packages\n 45. {snap}: Number of snap packages\n 46. {soar}: Number of soar packages\n 47. {sorcery}: Number of sorcery packages\n 48. {winget}: Number of winget packages\n 49. {xbps}: Number of xbps packages\n 50. {brew-all}: Total number of all brew packages\n 51. {flatpak-all}: Total number of all flatpak app packages\n 52. {guix-all}: Total number of all guix packages\n 53. {hpkg-all}: Total number of all hpkg packages\n 54. {nix-all}: Total number of all nix packages\n 55. {all}: Number of all packages",
|
||||
"type": "string"
|
||||
},
|
||||
"physicaldiskFormat": {
|
||||
"description": "Output format for the `PhysicalDisk` module. See Wiki for formatting syntax\n 1. {size}: Device size (formatted)\n 2. {name}: Device name\n 3. {interconnect}: Device interconnect type\n 4. {dev-path}: Device raw file path\n 5. {serial}: Serial number\n 6. {physical-type}: Device kind (SSD or HDD)\n 7. {removable-type}: Device kind (Removable or Fixed)\n 8. {readonly-type}: Device kind (Read-only or Read-write)\n 9. {revision}: Product revision\n 10. {temperature}: Device temperature (formatted)",
|
||||
"description": "Output format for the `PhysicalDisk` module. See Wiki for formatting syntax\n 1. {size}: Device size (formatted)\n 2. {name}: Device name *\n 3. {interconnect}: Device interconnect type\n 4. {dev-path}: Device raw file path *\n 5. {serial}: Serial number\n 6. {physical-type}: Device kind (SSD or HDD)\n 7. {removable-type}: Device kind (Removable or Fixed)\n 8. {readonly-type}: Device kind (Read-only or Read-write)\n 9. {revision}: Product revision\n 10. {temperature}: Device temperature (formatted)",
|
||||
"type": "string"
|
||||
},
|
||||
"physicalmemoryFormat": {
|
||||
@@ -462,11 +465,11 @@
|
||||
"type": "string"
|
||||
},
|
||||
"poweradapterFormat": {
|
||||
"description": "Output format for the `PowerAdapter` module. See Wiki for formatting syntax\n 1. {watts}: Power adapter watts\n 2. {name}: Power adapter name\n 3. {manufacturer}: Power adapter manufacturer\n 4. {model}: Power adapter model\n 5. {description}: Power adapter description\n 6. {serial}: Power adapter serial number",
|
||||
"description": "Output format for the `PowerAdapter` module. See Wiki for formatting syntax\n 1. {watts}: Power adapter watts\n 2. {name}: Power adapter name\n 3. {manufacturer}: Power adapter manufacturer\n 4. {model-name}: Power adapter model name\n 5. {description}: Power adapter description\n 6. {serial}: Power adapter serial number",
|
||||
"type": "string"
|
||||
},
|
||||
"processesFormat": {
|
||||
"description": "Output format for the `Processes` module. See Wiki for formatting syntax\n 1. {result}: Process count",
|
||||
"description": "Output format for the `Processes` module. See Wiki for formatting syntax\n 1. {result}: Process count\n 2. {threads}: Thread count",
|
||||
"type": "string"
|
||||
},
|
||||
"publicipFormat": {
|
||||
@@ -482,7 +485,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"swapFormat": {
|
||||
"description": "Output format for the `Swap` module. See Wiki for formatting syntax\n 1. {used}: Used size\n 2. {total}: Total size\n 3. {percentage}: Percentage used (num)\n 4. {percentage-bar}: Percentage used (bar)\n 5. {name}: Name",
|
||||
"description": "Output format for the `Swap` module. See Wiki for formatting syntax\n 1. {used}: Used size\n 2. {total}: Total size\n 3. {percentage}: Percentage used (num)\n 4. {percentage-bar}: Percentage used (bar)\n 5. {name}: Name *",
|
||||
"type": "string"
|
||||
},
|
||||
"terminalFormat": {
|
||||
@@ -554,7 +557,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"zpoolFormat": {
|
||||
"description": "Output format for the `Zpool` module. See Wiki for formatting syntax\n 1. {name}: Zpool name\n 2. {guid}: Zpool guid\n 3. {state}: Zpool state\n 4. {size-used}: Size used\n 5. {size-allocated}: Size allocated\n 6. {size-total}: Size total\n 7. {used-percentage}: Size used percentage num\n 8. {allocated-percentage}: Size allocated percentage num\n 9. {frag-percentage}: Fragmentation percentage num\n 10. {used-percentage-bar}: Size used percentage bar\n 11. {allocated-percentage-bar}: Size allocated percentage bar\n 12. {frag-percentage-bar}: Fragmentation percentage bar\n 13. {is-readonly}: Is read-only",
|
||||
"description": "Output format for the `Zpool` module. See Wiki for formatting syntax\n 1. {name}: Zpool name *\n 2. {guid}: Zpool guid *\n 3. {state}: Zpool state\n 4. {size-used}: Size used\n 5. {size-allocated}: Size allocated\n 6. {size-total}: Size total\n 7. {used-percentage}: Size used percentage num\n 8. {allocated-percentage}: Size allocated percentage num\n 9. {frag-percentage}: Fragmentation percentage num\n 10. {used-percentage-bar}: Size used percentage bar\n 11. {allocated-percentage-bar}: Size allocated percentage bar\n 12. {frag-percentage-bar}: Fragmentation percentage bar\n 13. {is-readonly}: Is read-only",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -782,8 +785,9 @@
|
||||
},
|
||||
"position": {
|
||||
"type": "string",
|
||||
"description": "Position of the logo",
|
||||
"description": "Position of the logo. Auto places a left-side logo above the output when the terminal is too narrow to reserve 32 columns for module output.",
|
||||
"enum": [
|
||||
"auto",
|
||||
"left",
|
||||
"top",
|
||||
"right"
|
||||
@@ -934,6 +938,17 @@
|
||||
"type": "boolean",
|
||||
"description": "Whether to detect and display component versions. Mainly for benchmarking",
|
||||
"default": true
|
||||
},
|
||||
"preload": {
|
||||
"description": "Scripts to run while the config is read, before any module runs",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"lua": {
|
||||
"type": "string",
|
||||
"description": "Lua script to run once, as soon as the config is read, for example `dofile('/path/to/helpers.lua')`"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1108,6 +1123,11 @@
|
||||
"const": "es",
|
||||
"description": "Spanish"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"const": "fi",
|
||||
"description": "Finnish"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"const": "fr",
|
||||
@@ -1576,6 +1596,7 @@
|
||||
"command",
|
||||
"colors",
|
||||
"cursor",
|
||||
"custom",
|
||||
"datetime",
|
||||
"display",
|
||||
"disk",
|
||||
@@ -1595,6 +1616,7 @@
|
||||
"loadavg",
|
||||
"locale",
|
||||
"localip",
|
||||
"logo",
|
||||
"media",
|
||||
"memory",
|
||||
"monitor",
|
||||
@@ -1744,6 +1766,24 @@
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"showType": {
|
||||
"description": "Which Bluetooth stacks to look for",
|
||||
"oneOf": [
|
||||
{
|
||||
"const": "classic",
|
||||
"description": "Look for BR/EDR devices only"
|
||||
},
|
||||
{
|
||||
"const": "le",
|
||||
"description": "Look for Bluetooth Low Energy devices only"
|
||||
},
|
||||
{
|
||||
"const": "both",
|
||||
"description": "Look for devices on either stack"
|
||||
}
|
||||
],
|
||||
"default": "both"
|
||||
},
|
||||
"percent": {
|
||||
"$ref": "#/$defs/percent"
|
||||
},
|
||||
@@ -1879,7 +1919,7 @@
|
||||
"$ref": "#/$defs/percent"
|
||||
},
|
||||
"ddcciSleep": {
|
||||
"description": "Sleep time in milliseconds between DDC/CI requests\nSet to null to skip DDC/CI detection\nSee <https://www.ddcutil.com/performance_options/#option-sleep-multiplier> for details",
|
||||
"description": "Sleep time in milliseconds between DDC/CI requests\nSet to null to skip DDC/CI detection",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null",
|
||||
@@ -2100,7 +2140,7 @@
|
||||
"showPeCoreCount": {
|
||||
"description": "Whether to detect and display CPU frequencies for different core types (for example, P-cores and E-cores), if supported",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
"default": true
|
||||
},
|
||||
"key": {
|
||||
"$ref": "#/$defs/key"
|
||||
@@ -2134,8 +2174,10 @@
|
||||
"const": "cpucache",
|
||||
"description": "Print CPU cache sizes"
|
||||
},
|
||||
"percent": {
|
||||
"$ref": "#/$defs/percent"
|
||||
"compact": {
|
||||
"type": "boolean",
|
||||
"description": "Print one line with a total per cache level instead of one line per level",
|
||||
"default": false
|
||||
},
|
||||
"key": {
|
||||
"$ref": "#/$defs/key"
|
||||
@@ -2254,6 +2296,7 @@
|
||||
"description": "Set the number of white spaces to print before the symbol",
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 4294967295,
|
||||
"default": 0
|
||||
},
|
||||
"block": {
|
||||
@@ -2265,6 +2308,7 @@
|
||||
"description": "Set the block width in spaces",
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 9,
|
||||
"default": 3
|
||||
},
|
||||
"range": {
|
||||
@@ -2322,7 +2366,7 @@
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"type": {
|
||||
"description": "Run a custom shell command",
|
||||
"description": "Run custom shell scripts",
|
||||
"const": "command"
|
||||
},
|
||||
"shell": {
|
||||
@@ -2337,10 +2381,24 @@
|
||||
"description": "Command text to execute",
|
||||
"type": "string"
|
||||
},
|
||||
"useStdErr": {
|
||||
"description": "Whether to use stderr instead of stdout for command output",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
"useOutput": {
|
||||
"description": "Which of the command's output streams to capture\nThe stream that is not captured is discarded",
|
||||
"type": "string",
|
||||
"oneOf": [
|
||||
{
|
||||
"const": "stdout",
|
||||
"description": "Capture stdout only and discard stderr"
|
||||
},
|
||||
{
|
||||
"const": "stderr",
|
||||
"description": "Capture stderr only and discard stdout"
|
||||
},
|
||||
{
|
||||
"const": "both",
|
||||
"description": "Capture both stdout and stderr"
|
||||
}
|
||||
],
|
||||
"default": "both"
|
||||
},
|
||||
"parallel": {
|
||||
"description": "Whether to execute the command in parallel with other commands\nThis can improve performance when using multiple commands, but may cause issues with some commands",
|
||||
@@ -2384,9 +2442,6 @@
|
||||
"const": "cursor",
|
||||
"description": "Print cursor style name"
|
||||
},
|
||||
"percent": {
|
||||
"$ref": "#/$defs/percent"
|
||||
},
|
||||
"key": {
|
||||
"$ref": "#/$defs/key"
|
||||
},
|
||||
@@ -2420,8 +2475,7 @@
|
||||
"const": "custom"
|
||||
},
|
||||
"key": {
|
||||
"description": "Leave empty to hide the key",
|
||||
"type": "string"
|
||||
"$ref": "#/$defs/key"
|
||||
},
|
||||
"keyColor": {
|
||||
"$ref": "#/$defs/keyColor"
|
||||
@@ -2453,9 +2507,6 @@
|
||||
"const": "datetime",
|
||||
"description": "Print the current date and time"
|
||||
},
|
||||
"percent": {
|
||||
"$ref": "#/$defs/percent"
|
||||
},
|
||||
"key": {
|
||||
"$ref": "#/$defs/key"
|
||||
},
|
||||
@@ -2665,7 +2716,7 @@
|
||||
},
|
||||
"useAvailable": {
|
||||
"type": "boolean",
|
||||
"description": "Use f_bavail (lpFreeBytesAvailableToCaller for Windows) instead of f_bfree to calculate used bytes\nMay be required for macOS to display correct results",
|
||||
"description": "Use f_bavail instead of f_bfree to calculate used bytes\nMay be required for macOS to display correct results\nOn Windows, set to true to make fastfetch honors the quota settings",
|
||||
"default": false
|
||||
},
|
||||
"percent": {
|
||||
@@ -2832,9 +2883,6 @@
|
||||
"const": "editor",
|
||||
"description": "Print information about the default editor (`$VISUAL` or `$EDITOR`)"
|
||||
},
|
||||
"percent": {
|
||||
"$ref": "#/$defs/percent"
|
||||
},
|
||||
"key": {
|
||||
"$ref": "#/$defs/key"
|
||||
},
|
||||
@@ -2867,9 +2915,6 @@
|
||||
"const": "font",
|
||||
"description": "Print system font names"
|
||||
},
|
||||
"percent": {
|
||||
"$ref": "#/$defs/percent"
|
||||
},
|
||||
"key": {
|
||||
"$ref": "#/$defs/key"
|
||||
},
|
||||
@@ -3775,7 +3820,9 @@
|
||||
"enum": [
|
||||
"am",
|
||||
"apk",
|
||||
"appimage",
|
||||
"brew",
|
||||
"cards",
|
||||
"choco",
|
||||
"crux",
|
||||
"dpkg",
|
||||
@@ -3784,10 +3831,13 @@
|
||||
"flatpak",
|
||||
"guix",
|
||||
"hpkg",
|
||||
"installrelease",
|
||||
"kiss",
|
||||
"linglong",
|
||||
"lpkg",
|
||||
"lpkgbuild",
|
||||
"macports",
|
||||
"moss",
|
||||
"mport",
|
||||
"nix",
|
||||
"opkg",
|
||||
@@ -3796,10 +3846,14 @@
|
||||
"paludis",
|
||||
"pisi",
|
||||
"pkg",
|
||||
"pkgsrc",
|
||||
"pkgtool",
|
||||
"porg",
|
||||
"rpm",
|
||||
"rum",
|
||||
"scoop",
|
||||
"snap",
|
||||
"soar",
|
||||
"sorcery",
|
||||
"winget",
|
||||
"xbps"
|
||||
@@ -3811,8 +3865,7 @@
|
||||
"description": "Enable all package managers",
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": []
|
||||
]
|
||||
},
|
||||
"combined": {
|
||||
"description": "Whether to combine related package managers into single counts (e.g., nix-system + nix-user = nix)",
|
||||
@@ -4039,10 +4092,9 @@
|
||||
"const": "publicip"
|
||||
},
|
||||
"url": {
|
||||
"description": "URL of the public IP detection server to use. Only HTTP is supported",
|
||||
"description": "URL of the public IP detection server to use. Only HTTP is supported\nIf empty, `http://ipinfo.io/json` is requested (`http://v6.ipinfo.io/json` when `ipv6` is set), with TCP Fast Open enabled. A non-empty URL disables both and makes `ipv6` have no effect",
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"default": "http://ipinfo.io/ip"
|
||||
"format": "url"
|
||||
},
|
||||
"timeout": {
|
||||
"description": "Time in milliseconds to wait for the public IP server to respond\nSet to 0 to disable the timeout",
|
||||
@@ -4100,6 +4152,7 @@
|
||||
"description": "Number of times to repeat the separator string, or 0 to auto-detect",
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 4294967295,
|
||||
"default": 0
|
||||
},
|
||||
"condition": {
|
||||
@@ -4804,6 +4857,9 @@
|
||||
"const": "wifi",
|
||||
"description": "Print connected Wi-Fi info (SSID, connection and security protocol)"
|
||||
},
|
||||
"percent": {
|
||||
"$ref": "#/$defs/percent"
|
||||
},
|
||||
"key": {
|
||||
"$ref": "#/$defs/key"
|
||||
},
|
||||
|
||||
Vendored
+10
-2
@@ -114,7 +114,7 @@ that 11.3 MB was `images/` fixtures, so the meaningful comparison is code:
|
||||
|
||||
## Local modifications
|
||||
|
||||
Three deliberate divergences from upstream. Re-apply all three when re-syncing.
|
||||
Four deliberate divergences from upstream. Re-apply all four when re-syncing.
|
||||
|
||||
1. **`sixel.h` — `SIXELAPI` is empty.** Upstream defines it as
|
||||
`__declspec(dllexport)` on Windows. This subset is compiled into `libfastfetch`
|
||||
@@ -152,6 +152,14 @@ Three deliberate divergences from upstream. Re-apply all three when re-syncing.
|
||||
not fill, so the identical bytes look correct there. It is also why Linux never showed
|
||||
the problem: that path goes through ImageMagick, which already sends `P2 = 1`.
|
||||
|
||||
4. **`output.c` — release the allocator reference if output allocation fails.**
|
||||
`sixel_output_new()` creates an allocator or takes an additional reference before
|
||||
allocating the output context. If that allocation fails, the reference is not owned by
|
||||
an output object and must be released before returning `SIXEL_BAD_ALLOCATION`.
|
||||
Fastfetch passes a null allocator, so without this cleanup the newly created allocator
|
||||
becomes unreachable on this error path. The successful path and returned error code are
|
||||
unchanged.
|
||||
|
||||
The vendored sources are also exempted from `-Wconversion` in `CMakeLists.txt`:
|
||||
upstream has ~160 implicit int→`unsigned char` narrowing warnings. Everything else
|
||||
(`-Wall -Wextra` and fastfetch's `-Werror=` set) applies unchanged, and the subset
|
||||
@@ -171,7 +179,7 @@ done
|
||||
cp /tmp/libsixel/include/sixel.h .
|
||||
cp /tmp/libsixel/LICENSE /tmp/libsixel/LICENSE.sixel /tmp/libsixel/LICENSE.pnmcolormap .
|
||||
|
||||
# 2. re-apply the three local modifications listed above
|
||||
# 2. re-apply the four local modifications listed above
|
||||
# 3. verify
|
||||
```
|
||||
|
||||
|
||||
Vendored
+1
@@ -59,6 +59,7 @@ sixel_output_new(
|
||||
sixel_helper_set_additional_message(
|
||||
"sixel_output_new: sixel_allocator_malloc() failed.");
|
||||
status = SIXEL_BAD_ALLOCATION;
|
||||
sixel_allocator_unref(allocator);
|
||||
goto end;
|
||||
}
|
||||
|
||||
|
||||
Vendored
+2
-1
@@ -6,6 +6,7 @@
|
||||
"modifications": [
|
||||
"sixel.h: SIXELAPI is empty, so the statically linked subset exports nothing",
|
||||
"config.h: hand-written, because only the vendored files read it",
|
||||
"tosixel.c: DCS P2 initialised to 1, so unset pixels are not filled with colour-table entry 0"
|
||||
"tosixel.c: DCS P2 initialised to 1, so unset pixels are not filled with colour-table entry 0",
|
||||
"output.c: release the allocator reference if output-context allocation fails"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include "fastfetch.h"
|
||||
|
||||
// A cache for a value that is built at most once per generation.
|
||||
//
|
||||
// The value itself is owned by the entry owner (usually a file scope `static`); the entry only
|
||||
// remembers whether that value is still valid and knows how to drop it. `--dynamic-interval`
|
||||
// starts a new generation for every round, dropping all the values that have been built so far,
|
||||
// so the modules re-detect them instead of replaying the first round's snapshot.
|
||||
//
|
||||
// An entry registers itself the first time it is used. There is therefore no central list to keep
|
||||
// in sync, and a cache cannot be forgotten in it. Do not use this type for state that is carried
|
||||
// across calls to derive a rate (cpuusage, diskio, netio, top): that is a baseline rather than a
|
||||
// snapshot, and dropping it makes the first sample of a round read as zero.
|
||||
//
|
||||
// The storage pointed to by `storage` must stay valid for the whole run; only the value behind it
|
||||
// is dropped by an invalidation.
|
||||
//
|
||||
// Not thread safe: entries are only touched from a single thread, on a frame boundary, while the
|
||||
// modules are not running.
|
||||
typedef struct FFcacheEntry {
|
||||
const char* name; // For diagnostics.
|
||||
void* storage; // Owned by the entry owner. May be nullptr.
|
||||
void (*init)(void* storage); // Optional. Runs whenever the entry is stale.
|
||||
void (*destroy)(void* storage); // Optional. Must be idempotent, and tolerate a partially initialized storage.
|
||||
bool initialized;
|
||||
bool registered;
|
||||
struct FFcacheEntry* next; // Internal: registry link.
|
||||
} FFcacheEntry;
|
||||
|
||||
// Returns `entry->storage`, running `init` on it first when the entry is stale. The entry is marked
|
||||
// initialized before `init` runs, so a call made from within `init` returns the partially built
|
||||
// value instead of recursing.
|
||||
[[gnu::nonnull(1)]] void* ffCacheGet(FFcacheEntry* entry);
|
||||
|
||||
// Staleness test for entries whose initialization needs arguments that are not part of the cache
|
||||
// identity (such as the cover file request of `ffDetectMedia`) and therefore cannot live behind
|
||||
// `init(void*)`. Returns true exactly once per generation: the caller must then build the value
|
||||
// itself.
|
||||
[[gnu::nonnull(1), nodiscard]] bool ffCacheBeginInit(FFcacheEntry* entry);
|
||||
|
||||
// Drops the cached value of one entry, so that the next request builds it again. Idempotent, and
|
||||
// safe on an entry that has never been used.
|
||||
[[gnu::nonnull(1)]] void ffCacheInvalidate(FFcacheEntry* entry);
|
||||
|
||||
// Drops every value that has been built so far. Called once per `--dynamic-interval` round.
|
||||
void ffCacheInvalidateAll(void);
|
||||
@@ -466,6 +466,12 @@ static inline bool ffStrbufInitJsonVal(FFstrbuf* strbuf, yyjson_val* jsonVal) {
|
||||
return ptr ? (uint32_t) (ptr - strbuf->chars) : strbuf->length;
|
||||
}
|
||||
|
||||
[[gnu::nonnull(1), gnu::pure, nodiscard]] static inline bool ffStrbufContainAnyC(const FFstrbuf* strbuf, const char* cs) {
|
||||
assert(cs != nullptr);
|
||||
|
||||
return strpbrk(strbuf->chars, cs) != nullptr;
|
||||
}
|
||||
|
||||
[[gnu::nonnull(1, 3), gnu::pure, nodiscard]] static inline uint32_t ffStrbufNextIndexS(const FFstrbuf* strbuf, uint32_t start, const char* str) {
|
||||
assert(start <= strbuf->length);
|
||||
|
||||
|
||||
@@ -0,0 +1,560 @@
|
||||
#include "common/android/applabel.h"
|
||||
#include "common/android/binder.h"
|
||||
#include "common/android/dex.h"
|
||||
#include "common/android/zip.h"
|
||||
#include "common/debug.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
// The package manager, and the one method that describes an app to a caller that is not the shell.
|
||||
// IPackageManager has always been part of framework.jar -- it is not one of the frameworks that
|
||||
// moved into an APEX -- so there is one path and no fallback.
|
||||
#define FF_ANDROID_LABEL_SERVICE "package"
|
||||
#define FF_ANDROID_LABEL_DESCRIPTOR "android.content.pm.IPackageManager"
|
||||
#define FF_ANDROID_LABEL_JAR "/system/framework/framework.jar"
|
||||
#define FF_ANDROID_LABEL_STUB "Landroid/content/pm/IPackageManager$Stub;"
|
||||
#define FF_ANDROID_LABEL_TRANSACTION "TRANSACTION_getApplicationInfo"
|
||||
|
||||
#define FF_ANDROID_LABEL_TABLE "resources.arsc"
|
||||
|
||||
// The longest package name AIDL accepts is 255 characters, and the request is one string16 of it, the
|
||||
// interface token, a 64-bit flags argument and a 32-bit user id.
|
||||
// clang-format off
|
||||
#define FF_ANDROID_LABEL_REQUEST_SIZE \
|
||||
(12 + ((4 + 2 * (sizeof(FF_ANDROID_LABEL_DESCRIPTOR) - 1) + 3) & ~3u) \
|
||||
+ ((4 + 2 * 255 + 3) & ~3u) + 8u + 4u)
|
||||
// clang-format on
|
||||
|
||||
// An ApplicationInfo parcel runs 2.0 to 3.2 KB on the test device, and the surplus is what a reply
|
||||
// with more split names and directories than any of those would spend.
|
||||
#define FF_ANDROID_LABEL_REPLY_SIZE (16 * 1024)
|
||||
|
||||
// Long enough for the `/data/app/~~<22 base64 chars>==/<package>-<22 chars>==/base.apk` the platform
|
||||
// installs to, with room to spare.
|
||||
#define FF_ANDROID_LABEL_PATH_SIZE 512
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// The resource table
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
//
|
||||
// `resources.arsc` is a ResTable: a header, a global string pool, and then one ResTable_package per
|
||||
// package it defines, each carrying its own pools for type names and entry keys followed by the
|
||||
// types themselves. Resolving `labelRes` is two indices deep -- the package's number names the
|
||||
// package and the type, the low half names the entry -- and every step below reads the file rather
|
||||
// than a table of what the file is expected to hold, because the layout is the platform's and the
|
||||
// numbers are per APK.
|
||||
|
||||
// ResChunk_header: type(2) headerSize(2) size(4). Every chunk in the file starts with one.
|
||||
#define FF_ARSC_TABLE_TYPE 0x0002
|
||||
#define FF_ARSC_STRING_POOL_TYPE 0x0001
|
||||
#define FF_ARSC_TABLE_PACKAGE_TYPE 0x0200
|
||||
#define FF_ARSC_TABLE_TYPE_TYPE 0x0201
|
||||
|
||||
// ResStringPool: header(8) stringCount(4) styleCount(4) flags(4) stringsStart(4) stylesStart(4)
|
||||
#define FF_ARSC_POOL_HEADER_SIZE 28
|
||||
#define FF_ARSC_OFF_POOL_CHUNK_SIZE 4
|
||||
#define FF_ARSC_OFF_POOL_COUNT 8
|
||||
#define FF_ARSC_OFF_POOL_FLAGS 16
|
||||
#define FF_ARSC_OFF_POOL_STRINGS_START 20
|
||||
|
||||
// A UTF-8 pool says so with this bit, and then holds the strings as UTF-8 rather than as UTF-16.
|
||||
#define FF_ARSC_POOL_FLAG_UTF8 0x0100
|
||||
|
||||
// ResTable_package: header(8) id(4) name(256, a UTF-16 array) typeStrings(4) lastPublicType(4) ...
|
||||
#define FF_ARSC_OFF_PACKAGE_ID 8
|
||||
#define FF_ARSC_OFF_PACKAGE_TYPE_STRINGS (8 + 4 + 256)
|
||||
|
||||
// ResTable_type: header(8) id(1) flags(1) reserved(2) entryCount(4) entriesStart(4) config(...)
|
||||
#define FF_ARSC_OFF_TYPE_ID 8
|
||||
#define FF_ARSC_OFF_TYPE_FLAGS 9
|
||||
#define FF_ARSC_OFF_TYPE_ENTRY_COUNT 12
|
||||
#define FF_ARSC_OFF_TYPE_ENTRIES_START 16
|
||||
#define FF_ARSC_OFF_TYPE_CONFIG 20
|
||||
|
||||
// The two shapes a type can take that this does not read: a sparse type stores its offsets as
|
||||
// index-offset pairs rather than as a flat table, and a 16-bit one stores them at half width. Neither
|
||||
// is in the APKs measured, and both would resolve an entry to the wrong offset rather than to none,
|
||||
// so they are refused rather than read as if they were the plain shape.
|
||||
#define FF_ARSC_TYPE_FLAG_SPARSE 0x01
|
||||
#define FF_ARSC_TYPE_FLAG_OFFSET16 0x02
|
||||
|
||||
// ResTable_entry: size(2) flags(2), where size is the header's own length and the value follows it.
|
||||
#define FF_ARSC_ENTRY_FLAG_COMPLEX 0x0001
|
||||
|
||||
// Res_value: size(2) res0(1) dataType(1) data(4). TYPE_STRING makes data an index into the global
|
||||
// string pool; every other type is either a number or a reference to another resource.
|
||||
#define FF_ARSC_OFF_VALUE_DATA_TYPE 3
|
||||
#define FF_ARSC_OFF_VALUE_DATA 4
|
||||
#define FF_ARSC_TYPE_STRING 0x03
|
||||
|
||||
// A type chunk's offset table has one word per entry, and this is what an entry with no value in that
|
||||
// configuration looks like. It is not zero: offset zero is a real entry, at the start of the block.
|
||||
#define FF_ARSC_NO_ENTRY 0xffffffffu
|
||||
|
||||
// The type a label resolves to. `string` is what `@string/...` compiles to, and it is what tells a
|
||||
// label from the icons and banners that sit beside it in the same reply -- those point at `mipmap`
|
||||
// or `drawable` entries whose value is a path inside the APK, so resolving them would hand back
|
||||
// `res/9w.png` instead of a name.
|
||||
#define FF_ANDROID_LABEL_STRING_TYPE "string"
|
||||
|
||||
static uint16_t readU16(const uint8_t* p) {
|
||||
return (uint16_t) ((uint16_t) p[0] | (uint16_t) ((uint16_t) p[1] << 8));
|
||||
}
|
||||
|
||||
static uint32_t readU32(const uint8_t* p) {
|
||||
return (uint32_t) p[0] | ((uint32_t) p[1] << 8) | ((uint32_t) p[2] << 16) | ((uint32_t) p[3] << 24);
|
||||
}
|
||||
|
||||
// One ResStringPool, of which a table has three.
|
||||
typedef struct FFAndroidLabelPool {
|
||||
const uint8_t* table;
|
||||
size_t tableSize;
|
||||
size_t offset;
|
||||
uint32_t chunkSize;
|
||||
uint32_t count;
|
||||
uint32_t headerSize;
|
||||
uint32_t stringsStart;
|
||||
bool utf8;
|
||||
} FFAndroidLabelPool;
|
||||
|
||||
[[gnu::nonnull(2), nodiscard]] static bool poolInit(FFAndroidLabelPool* pool, const uint8_t* table, size_t tableSize, size_t offset) {
|
||||
if (offset > tableSize || tableSize - offset < FF_ARSC_POOL_HEADER_SIZE) {
|
||||
return false;
|
||||
}
|
||||
if (readU16(table + offset) != FF_ARSC_STRING_POOL_TYPE) {
|
||||
return false;
|
||||
}
|
||||
pool->table = table;
|
||||
pool->tableSize = tableSize;
|
||||
pool->offset = offset;
|
||||
pool->chunkSize = readU32(table + offset + FF_ARSC_OFF_POOL_CHUNK_SIZE);
|
||||
pool->count = readU32(table + offset + FF_ARSC_OFF_POOL_COUNT);
|
||||
pool->headerSize = readU16(table + offset + 2);
|
||||
pool->stringsStart = readU32(table + offset + FF_ARSC_OFF_POOL_STRINGS_START);
|
||||
pool->utf8 = (readU32(table + offset + FF_ARSC_OFF_POOL_FLAGS) & FF_ARSC_POOL_FLAG_UTF8) != 0;
|
||||
return pool->headerSize >= FF_ARSC_POOL_HEADER_SIZE && pool->chunkSize > pool->headerSize;
|
||||
}
|
||||
|
||||
// The length in front of a string. A UTF-8 pool writes one or two bytes, a UTF-16 pool one or two
|
||||
// 16-bit words, and the top bit of the first says which of the two it is.
|
||||
[[gnu::nonnull(1, 3), nodiscard]] static bool readLength8(const FFAndroidLabelPool* pool, size_t* position, uint32_t* length) {
|
||||
if (*position >= pool->tableSize) {
|
||||
return false;
|
||||
}
|
||||
uint32_t value = pool->table[(*position)++];
|
||||
if (value & 0x80) {
|
||||
if (*position >= pool->tableSize) {
|
||||
return false;
|
||||
}
|
||||
value = ((value & 0x7F) << 8) | pool->table[(*position)++];
|
||||
}
|
||||
*length = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
[[gnu::nonnull(1, 3), nodiscard]] static bool readLength16(const FFAndroidLabelPool* pool, size_t* position, uint32_t* length) {
|
||||
if (*position + 2 > pool->tableSize) {
|
||||
return false;
|
||||
}
|
||||
uint32_t value = readU16(pool->table + *position);
|
||||
*position += 2;
|
||||
if (value & 0x8000) {
|
||||
if (*position + 2 > pool->tableSize) {
|
||||
return false;
|
||||
}
|
||||
value = ((value & 0x7FFF) << 16) | readU16(pool->table + *position);
|
||||
*position += 2;
|
||||
}
|
||||
*length = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
[[gnu::nonnull(1)]] static void appendCodePoint(FFstrbuf* result, uint32_t code) {
|
||||
char encoded[4] = {};
|
||||
uint32_t length = 0;
|
||||
if (code < 0x80) {
|
||||
encoded[length++] = (char) code;
|
||||
} else if (code < 0x800) {
|
||||
encoded[length++] = (char) (0xC0 | (code >> 6));
|
||||
encoded[length++] = (char) (0x80 | (code & 0x3F));
|
||||
} else if (code < 0x10000) {
|
||||
encoded[length++] = (char) (0xE0 | (code >> 12));
|
||||
encoded[length++] = (char) (0x80 | ((code >> 6) & 0x3F));
|
||||
encoded[length++] = (char) (0x80 | (code & 0x3F));
|
||||
} else {
|
||||
encoded[length++] = (char) (0xF0 | (code >> 18));
|
||||
encoded[length++] = (char) (0x80 | ((code >> 12) & 0x3F));
|
||||
encoded[length++] = (char) (0x80 | ((code >> 6) & 0x3F));
|
||||
encoded[length++] = (char) (0x80 | (code & 0x3F));
|
||||
}
|
||||
ffStrbufAppendNS(result, length, encoded);
|
||||
}
|
||||
|
||||
// One string out of a pool.
|
||||
//
|
||||
// The two lengths in front of a UTF-8 string are a *character* count and a *byte* count, and they
|
||||
// differ for everything outside ASCII. Decoding `characters` bytes is the trap: a label of a few
|
||||
// multi-byte characters comes back as its first character followed by replacement bytes. Measured on
|
||||
// the test device, where every resource table read is a UTF-8 pool, this was the difference between
|
||||
// the right label and mojibake.
|
||||
[[gnu::nonnull(1, 3), nodiscard]] static bool poolGet(const FFAndroidLabelPool* pool, uint32_t index, FFstrbuf* result) {
|
||||
if (index >= pool->count) {
|
||||
return false;
|
||||
}
|
||||
const size_t offsets = pool->offset + pool->headerSize;
|
||||
if (offsets + ((size_t) index + 1) * 4 > pool->tableSize) {
|
||||
return false;
|
||||
}
|
||||
size_t position = pool->offset + pool->stringsStart + readU32(pool->table + offsets + (size_t) index * 4);
|
||||
if (position >= pool->tableSize) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pool->utf8) {
|
||||
uint32_t characters = 0;
|
||||
uint32_t bytes = 0;
|
||||
if (!readLength8(pool, &position, &characters) || !readLength8(pool, &position, &bytes)) {
|
||||
return false;
|
||||
}
|
||||
// An empty string is reported as a miss rather than written: the caller sets its fallback
|
||||
// before asking, and ffStrbufSetNS() with a length of zero *clears* the buffer, which would
|
||||
// throw that fallback away instead of leaving it alone.
|
||||
if (bytes == 0 || bytes > pool->tableSize - position) {
|
||||
return false;
|
||||
}
|
||||
ffStrbufSetNS(result, bytes, (const char*) (pool->table + position));
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t units = 0;
|
||||
if (!readLength16(pool, &position, &units)) {
|
||||
return false;
|
||||
}
|
||||
if (units == 0 || (size_t) units * 2 > pool->tableSize - position) {
|
||||
return false;
|
||||
}
|
||||
for (uint32_t i = 0; i < units; i++) {
|
||||
uint32_t code = readU16(pool->table + position + (size_t) i * 2);
|
||||
// A code point outside the basic plane arrives as a surrogate pair, and the two halves are
|
||||
// not characters of their own: half of a pair encoded on its own is not valid UTF-8.
|
||||
if (code >= 0xD800 && code <= 0xDBFF && i + 1 < units) {
|
||||
const uint32_t low = readU16(pool->table + position + ((size_t) i + 1) * 2);
|
||||
if (low >= 0xDC00 && low <= 0xDFFF) {
|
||||
code = 0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
appendCodePoint(result, code);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// The one ResTable_package this table defines, with the two pools a lookup starts from.
|
||||
typedef struct FFAndroidLabelTable {
|
||||
const uint8_t* data;
|
||||
size_t size;
|
||||
FFAndroidLabelPool values;
|
||||
FFAndroidLabelPool types;
|
||||
size_t packageOffset;
|
||||
size_t packageHeaderSize;
|
||||
size_t packageSize;
|
||||
uint32_t packageId;
|
||||
} FFAndroidLabelTable;
|
||||
|
||||
[[gnu::nonnull(1), nodiscard]] static bool tableInit(FFAndroidLabelTable* table, const uint8_t* data, size_t size) {
|
||||
// ResTable_header: header(8) packageCount(4). The header's own size is where the global pool
|
||||
// starts, which is 12 in every table measured -- but it is the field rather than the number.
|
||||
if (size < 12 || readU16(data) != FF_ARSC_TABLE_TYPE) {
|
||||
return false;
|
||||
}
|
||||
table->data = data;
|
||||
table->size = size;
|
||||
const uint32_t headerSize = readU16(data + 2);
|
||||
if (!poolInit(&table->values, data, size, headerSize)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t position = (size_t) headerSize + table->values.chunkSize;
|
||||
while (position + 8 <= size) {
|
||||
const uint16_t type = readU16(data + position);
|
||||
const uint16_t chunkHeader = readU16(data + position + 2);
|
||||
const uint32_t chunkSize = readU32(data + position + 4);
|
||||
if (chunkSize < 8 || chunkSize > size - position) {
|
||||
break;
|
||||
}
|
||||
// A table can define several packages -- an app that links a shared library's resources has
|
||||
// more than one -- and the id in the resource reference is what picks the one to read.
|
||||
if (type == FF_ARSC_TABLE_PACKAGE_TYPE && chunkSize >= chunkHeader && chunkHeader >= FF_ARSC_OFF_PACKAGE_TYPE_STRINGS + 4) {
|
||||
table->packageOffset = position;
|
||||
table->packageHeaderSize = chunkHeader;
|
||||
table->packageSize = chunkSize;
|
||||
table->packageId = readU32(data + position + FF_ARSC_OFF_PACKAGE_ID);
|
||||
if (!poolInit(&table->types, data, size, position + readU32(data + position + FF_ARSC_OFF_PACKAGE_TYPE_STRINGS))) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
position += chunkSize;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// A type chunk whose ResTable_config is all zeroes is the one that applies when nothing about the
|
||||
// device or the locale matches a more specific chunk. An app with a translation has several chunks
|
||||
// for one type, and picking the first one that happens along would answer with whatever language it
|
||||
// was written in.
|
||||
[[gnu::nonnull(1), nodiscard]] static bool isDefaultConfig(const uint8_t* config, size_t length) {
|
||||
if (length == 0) {
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
if (config[i] != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
[[gnu::nonnull(1, 5), nodiscard]] static bool resolveIn(const FFAndroidLabelTable* table, uint32_t typeId, uint32_t entryId, bool defaultOnly, FFstrbuf* result) {
|
||||
const size_t end = table->packageOffset + table->packageSize;
|
||||
for (size_t position = table->packageOffset + table->packageHeaderSize; position + 8 <= end;) {
|
||||
const uint16_t type = readU16(table->data + position);
|
||||
const uint16_t header = readU16(table->data + position + 2);
|
||||
const uint32_t size = readU32(table->data + position + 4);
|
||||
if (size < 8 || size > end - position) {
|
||||
return false;
|
||||
}
|
||||
if (type == FF_ARSC_TABLE_TYPE_TYPE && header >= FF_ARSC_OFF_TYPE_CONFIG) {
|
||||
const uint8_t id = table->data[position + FF_ARSC_OFF_TYPE_ID];
|
||||
const uint8_t flags = table->data[position + FF_ARSC_OFF_TYPE_FLAGS];
|
||||
if (id == typeId && (flags & (FF_ARSC_TYPE_FLAG_SPARSE | FF_ARSC_TYPE_FLAG_OFFSET16)) == 0 && size >= header) {
|
||||
const uint32_t entryCount = readU32(table->data + position + FF_ARSC_OFF_TYPE_ENTRY_COUNT);
|
||||
const uint32_t entriesStart = readU32(table->data + position + FF_ARSC_OFF_TYPE_ENTRIES_START);
|
||||
const size_t offsets = position + header;
|
||||
if (entryId < entryCount && offsets + ((size_t) entryId + 1) * 4 <= end) {
|
||||
if (defaultOnly && !isDefaultConfig(table->data + position + FF_ARSC_OFF_TYPE_CONFIG, (size_t) header - FF_ARSC_OFF_TYPE_CONFIG)) {
|
||||
position += size;
|
||||
continue;
|
||||
}
|
||||
const uint32_t offset = readU32(table->data + offsets + (size_t) entryId * 4);
|
||||
// `entriesStart` is relative to the chunk, not to the package, and both it and
|
||||
// the entry's own offset come out of the file.
|
||||
if (offset != FF_ARSC_NO_ENTRY && entriesStart <= size && offset <= size - entriesStart) {
|
||||
const size_t entry = position + entriesStart + offset;
|
||||
const size_t value = entry + readU16(table->data + entry);
|
||||
if (value >= entry && value + 8 <= end && (readU16(table->data + entry + 2) & FF_ARSC_ENTRY_FLAG_COMPLEX) == 0
|
||||
&& table->data[value + FF_ARSC_OFF_VALUE_DATA_TYPE] == FF_ARSC_TYPE_STRING) {
|
||||
return poolGet(&table->values, readU32(table->data + value + FF_ARSC_OFF_VALUE_DATA), result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
position += size;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Resolves one resource id to its string, and only when the type it names is a `string`.
|
||||
//
|
||||
// The type name is the whole reason this reads the package's type pool rather than trusting the id:
|
||||
// `labelRes` and the icon, logo and banner ids that sit beside it in the same reply are all resource
|
||||
// ids of the same package, and the difference between them is the type they point at. A `mipmap` id
|
||||
// resolves perfectly well -- to `res/9w.png`.
|
||||
[[gnu::nonnull(1, 3), nodiscard]] static bool tableResolve(const FFAndroidLabelTable* table, uint32_t resourceId, FFstrbuf* result) {
|
||||
const uint32_t typeId = (resourceId >> 16) & 0xFF;
|
||||
if (((resourceId >> 24) & 0xFF) != table->packageId || typeId == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FFstrbuf typeName;
|
||||
ffStrbufInit(&typeName);
|
||||
const bool isString = poolGet(&table->types, typeId - 1, &typeName) && ffStrbufEqualS(&typeName, FF_ANDROID_LABEL_STRING_TYPE);
|
||||
ffStrbufDestroy(&typeName);
|
||||
if (!isString) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The configuration that applies is the unqualified one; a chunk that only applies to a locale
|
||||
// or a density is the fallback rather than the answer, which is what the second pass is for.
|
||||
return resolveIn(table, typeId, resourceId & 0xFFFF, true, result)
|
||||
|| resolveIn(table, typeId, resourceId & 0xFFFF, false, result);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// The reply
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
// The reply's own copy of the APK path. `sourceDir` is a string in the ApplicationInfo parcel and it
|
||||
// ends in `.apk`; nothing else in the reply does, which is what makes it findable without walking
|
||||
// the parcel's field order -- that would mean recovering `ApplicationInfo.writeToParcel` out of the
|
||||
// device's dex and then framing sixty fields of unknown width, to reach one string that is already
|
||||
// recognizable.
|
||||
[[gnu::nonnull(1, 3), nodiscard]] static bool findApkPath(const uint8_t* data, size_t size, char* result, size_t capacity) {
|
||||
for (size_t i = 8; i < size; i++) {
|
||||
if (data[i] != '/') {
|
||||
continue;
|
||||
}
|
||||
size_t end = i;
|
||||
while (end < size && data[end] >= 0x20 && data[end] < 0x7F) {
|
||||
end++;
|
||||
}
|
||||
const size_t length = end - i;
|
||||
if (length >= 5 && length < capacity && memcmp(data + end - 4, ".apk", 4) == 0) {
|
||||
memcpy(result, data + i, length);
|
||||
result[length] = '\0';
|
||||
return true;
|
||||
}
|
||||
i = end;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
typedef struct FFAndroidLabelMapping {
|
||||
const uint8_t* data;
|
||||
size_t size;
|
||||
} FFAndroidLabelMapping;
|
||||
|
||||
[[gnu::nonnull(1)]] static void labelUnmap(FFAndroidLabelMapping* mapping) {
|
||||
if (mapping->data != nullptr) {
|
||||
munmap((void*) mapping->data, mapping->size);
|
||||
}
|
||||
}
|
||||
|
||||
[[gnu::nonnull(2), nodiscard]] static const char* labelMap(const char* path, FFAndroidLabelMapping* mapping) {
|
||||
const int fd = open(path, O_RDONLY | O_CLOEXEC);
|
||||
if (fd < 0) {
|
||||
FF_DEBUG("open(%s) failed: %s", path, strerror(errno));
|
||||
return "open(apk) failed";
|
||||
}
|
||||
|
||||
struct stat status = {};
|
||||
const bool failed = fstat(fd, &status) != 0;
|
||||
if (failed || status.st_size <= 0) {
|
||||
FF_DEBUG("fstat(%s) reported %s", path, failed ? strerror(errno) : "an empty file");
|
||||
close(fd);
|
||||
return "fstat(apk) failed";
|
||||
}
|
||||
|
||||
void* mapped = mmap(nullptr, (size_t) status.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
|
||||
close(fd);
|
||||
if (mapped == MAP_FAILED) {
|
||||
FF_DEBUG("mmap(%s, %lld bytes) failed: %s", path, (long long) status.st_size, strerror(errno));
|
||||
return "mmap(apk) failed";
|
||||
}
|
||||
mapping->data = mapped;
|
||||
mapping->size = (size_t) status.st_size;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// One reply buffer for every call this file makes, and it makes one per query.
|
||||
static uint8_t s_reply[FF_ANDROID_LABEL_REPLY_SIZE];
|
||||
|
||||
const char* ffAndroidGetAppLabel(const char* packageName, FFstrbuf* result) {
|
||||
// The transaction code comes out of the jar rather than from a table here: it is a property of
|
||||
// the `.aidl` the device's ROM was built from, and a vendor that inserts a method ahead of this
|
||||
// one shifts every code behind it. See common/android/dex.h.
|
||||
int32_t transaction = FF_DEX_STATIC_INT_UNRESOLVED;
|
||||
const FFDexStaticIntRequest lookup = {
|
||||
.classDescriptor = FF_ANDROID_LABEL_STUB,
|
||||
.fieldName = FF_ANDROID_LABEL_TRANSACTION,
|
||||
.result = &transaction,
|
||||
};
|
||||
const char* error = ffDexStaticInts(FF_ANDROID_LABEL_JAR, &lookup, 1);
|
||||
if (error != nullptr) {
|
||||
return error;
|
||||
}
|
||||
if (transaction == FF_DEX_STATIC_INT_UNRESOLVED) {
|
||||
return "framework.jar does not declare IPackageManager.getApplicationInfo";
|
||||
}
|
||||
|
||||
[[gnu::cleanup(ffBinderClose)]] FFBinder binder = { .fd = -1 };
|
||||
error = ffBinderOpen(&binder);
|
||||
if (error != nullptr) {
|
||||
return error;
|
||||
}
|
||||
|
||||
[[gnu::cleanup(ffBinderServiceHandleRelease)]] FFBinderServiceHandle service = { .binder = &binder };
|
||||
error = ffBinderLookupService(&binder, FF_ANDROID_LABEL_SERVICE, FF_BINDER_SM_GET_SERVICE, &service.handle);
|
||||
if (error != nullptr) {
|
||||
FF_DEBUG("Looking up \"%s\" failed: %s", FF_ANDROID_LABEL_SERVICE, error);
|
||||
return error;
|
||||
}
|
||||
|
||||
uint8_t request[FF_ANDROID_LABEL_REQUEST_SIZE];
|
||||
FFBinderParcel parcel = ffBinderParcelCreate(request, sizeof(request));
|
||||
ffBinderParcelPutInterfaceToken(&parcel, FF_ANDROID_LABEL_DESCRIPTOR);
|
||||
ffBinderParcelPutString16(&parcel, packageName);
|
||||
// `getApplicationInfo(String, long flags, int userId)`. The flags argument is 64-bit from Android
|
||||
// 13 on and 32-bit before it, and the wider one is what works on both: a release that reads an
|
||||
// int takes the low word and leaves the zero high word unread, because an AIDL stub stops
|
||||
// reading once its own arguments are in. The narrow one is not symmetric -- a release that reads
|
||||
// a long would take half of its own flags argument for the user id.
|
||||
ffBinderParcelPutU64(&parcel, 0);
|
||||
ffBinderParcelPutI32(&parcel, 0);
|
||||
|
||||
FFBinderReply reply = ffBinderReplyCreate(s_reply, sizeof(s_reply));
|
||||
error = ffBinderTransact(&binder, service.handle, (uint32_t) transaction, 0, &parcel, &reply);
|
||||
if (error != nullptr) {
|
||||
return error;
|
||||
}
|
||||
if (ffBinderReplyIsStatus(&reply)) {
|
||||
return "The package manager rejected the request";
|
||||
}
|
||||
// The AIDL status first, then the presence marker of a nullable Parcelable: a package the caller
|
||||
// is not allowed to see answers an empty ApplicationInfo rather than an error.
|
||||
if (ffBinderReadI32(reply.data, reply.size, 0) != 0) {
|
||||
return "The package manager refused to describe the app";
|
||||
}
|
||||
if (ffBinderReadI32(reply.data, reply.size, 4) == 0) {
|
||||
FF_DEBUG("The package manager described \"%s\" as an empty ApplicationInfo", packageName);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
char path[FF_ANDROID_LABEL_PATH_SIZE];
|
||||
if (!findApkPath(reply.data, reply.size, path, sizeof(path))) {
|
||||
return "The reply carries no APK path";
|
||||
}
|
||||
FF_DEBUG("\"%s\" is installed at \"%s\"", packageName, path);
|
||||
|
||||
[[gnu::cleanup(labelUnmap)]] FFAndroidLabelMapping mapping = {};
|
||||
error = labelMap(path, &mapping);
|
||||
if (error != nullptr) {
|
||||
return error;
|
||||
}
|
||||
|
||||
FFAndroidZipEntry table = {};
|
||||
error = ffAndroidZipFindEntry(mapping.data, mapping.size, FF_ANDROID_LABEL_TABLE, &table);
|
||||
if (error != nullptr) {
|
||||
return error;
|
||||
}
|
||||
if (table.method != FF_ZIP_METHOD_STORED) {
|
||||
FF_DEBUG("\"%s\" stores resources.arsc with compression method %u", packageName, (unsigned) table.method);
|
||||
return "The resource table is stored compressed";
|
||||
}
|
||||
|
||||
FFAndroidLabelTable resources = {};
|
||||
if (!tableInit(&resources, table.payload, table.payloadSize)) {
|
||||
return "The resource table could not be read";
|
||||
}
|
||||
|
||||
// Every word of the reply shaped like one of this package's resource ids, in the order they
|
||||
// appear, and the first one that resolves to a `string` is the label. See the header for what
|
||||
// that rule was audited against and why it beats recovering the parcel's field order.
|
||||
for (size_t offset = 8; offset + 4 <= reply.size; offset += 4) {
|
||||
if (tableResolve(&resources, readU32(reply.data + offset), result)) {
|
||||
FF_DEBUG("\"%s\" shows itself as \"%s\"", packageName, result->chars);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
FF_DEBUG("No resource id in the reply of \"%s\" resolves to a string", packageName);
|
||||
return nullptr;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "fastfetch.h"
|
||||
|
||||
// The name an app presents to the user, which is not its package name: an app installed as
|
||||
// `com.termux` presents itself as `Termux:Launcher` on the launcher, in its notifications and on
|
||||
// the settings screen.
|
||||
//
|
||||
// Every route that answers with an *already resolved* label is closed to an app UID, and each one
|
||||
// was measured on the test device rather than assumed:
|
||||
//
|
||||
// * `dumpsys package` needs android.permission.DUMP.
|
||||
// * `cmd package dump` is filtered down to 929 bytes for an app UID, against 1.2 MB for the shell.
|
||||
// * The task descriptions the activity service hands out only ever describe the caller's own task.
|
||||
// * `getApplicationInfo` answers with `labelRes` -- a resource id -- and leaves `nonLocalizedLabel`
|
||||
// null, so there is no string in the reply to read.
|
||||
//
|
||||
// What is open is the APK itself. `getApplicationInfo` carries its path (`sourceDir`), APKs are
|
||||
// world readable, and the `resources.arsc` inside one is *stored* rather than deflated -- the
|
||||
// platform expects to mmap it -- so resolving an id out of it needs no decompressor. That is the
|
||||
// route this takes: one transaction, one mapping, no fork and no child process.
|
||||
//
|
||||
// The label is the resource id in the reply whose type is `string`. That is a rule about the reply
|
||||
// rather than about the parcel's field order, and it was audited against the manifest's own
|
||||
// `android:label` over every package installed on the test device: nine in ten have exactly one
|
||||
// `string` candidate and it is the right one every time, a handful more have two and the first is
|
||||
// still the right one, and the rest have none because their label is a literal string rather than a
|
||||
// reference (`android:label="Example"`), so the reply carries no id at all. No package resolved to
|
||||
// a wrong string; the counts behind that are in doc/app-label.md.
|
||||
//
|
||||
// Writes the label into `result` when it can be resolved, and leaves `result` alone otherwise --
|
||||
// which includes the cases above where there is nothing to resolve. The caller sets its fallback
|
||||
// first, so that an unresolved label costs nothing but the query.
|
||||
//
|
||||
// Returns nullptr on success, a static message when the query itself could not be made. An error is
|
||||
// not a fallback: it says the label was not looked for, not that the app has none.
|
||||
[[gnu::nonnull(1, 2), nodiscard]] const char* ffAndroidGetAppLabel(const char* packageName, FFstrbuf* result);
|
||||
+113
-16
@@ -1,5 +1,8 @@
|
||||
#include "common/android/binder.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/io.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
@@ -7,14 +10,18 @@
|
||||
const char* ffBinderOpen(FFBinder* binder) {
|
||||
*binder = (FFBinder) { .fd = -1, .shared = nullptr, .sharedSize = 0, .protocolVersion = 0 };
|
||||
|
||||
const int fd = open(FF_BINDER_DEVICE, O_RDWR | O_CLOEXEC);
|
||||
FF_AUTO_CLOSE_FD int fd = open(FF_BINDER_DEVICE, O_RDWR | O_CLOEXEC);
|
||||
if (fd < 0) {
|
||||
FF_DEBUG("open(" FF_BINDER_DEVICE ") failed: %s", strerror(errno));
|
||||
return "Failed to open " FF_BINDER_DEVICE;
|
||||
}
|
||||
|
||||
struct binder_version version = { .protocol_version = 0 };
|
||||
if (ioctl(fd, BINDER_VERSION, &version) != 0 || version.protocol_version != BINDER_CURRENT_PROTOCOL_VERSION) {
|
||||
close(fd);
|
||||
const int versionIoctl = ioctl(fd, BINDER_VERSION, &version);
|
||||
if (versionIoctl != 0 || version.protocol_version != BINDER_CURRENT_PROTOCOL_VERSION) {
|
||||
FF_DEBUG("BINDER_VERSION reported protocol %u, expected %u (%s)",
|
||||
version.protocol_version, BINDER_CURRENT_PROTOCOL_VERSION,
|
||||
versionIoctl != 0 ? strerror(errno) : "version mismatch");
|
||||
return "Unsupported binder protocol version";
|
||||
}
|
||||
|
||||
@@ -22,7 +29,7 @@ const char* ffBinderOpen(FFBinder* binder) {
|
||||
// addresses in BR_REPLY, so it has to stay mapped for as long as the handle is used.
|
||||
void* shared = mmap(nullptr, FF_BINDER_SHARED_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, fd, 0);
|
||||
if (shared == MAP_FAILED) {
|
||||
close(fd);
|
||||
FF_DEBUG("mmap(" FF_BINDER_DEVICE ") failed: %s", strerror(errno));
|
||||
return "Failed to mmap " FF_BINDER_DEVICE;
|
||||
}
|
||||
|
||||
@@ -32,6 +39,7 @@ const char* ffBinderOpen(FFBinder* binder) {
|
||||
.sharedSize = FF_BINDER_SHARED_SIZE,
|
||||
.protocolVersion = version.protocol_version,
|
||||
};
|
||||
fd = -1; // ownership moved into *binder, which ffBinderClose() lets go
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -47,13 +55,16 @@ void ffBinderClose(FFBinder* binder) {
|
||||
binder->sharedSize = 0;
|
||||
}
|
||||
|
||||
const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, const FFBinderParcel* parcel, FFBinderReply* reply) {
|
||||
const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, uint32_t flags, const FFBinderParcel* parcel, FFBinderReply* reply) {
|
||||
reply->size = 0;
|
||||
reply->code = 0;
|
||||
reply->flags = 0;
|
||||
reply->handleCount = 0;
|
||||
reply->fdCount = 0;
|
||||
|
||||
if (parcel->truncated) {
|
||||
FF_DEBUG("The parcel was truncated at %zu bytes, the caller buffer holds %zu",
|
||||
parcel->size, parcel->capacity);
|
||||
return "Binder parcel does not fit the caller buffer";
|
||||
}
|
||||
|
||||
@@ -62,7 +73,7 @@ const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, c
|
||||
.target = { .handle = handle },
|
||||
.cookie = 0,
|
||||
.code = code,
|
||||
.flags = 0, // a synchronous call; TF_ONE_WAY is the only flag the kernel acts on here
|
||||
.flags = flags,
|
||||
.sender_pid = 0,
|
||||
.sender_euid = 0,
|
||||
.data_size = parcel->size,
|
||||
@@ -85,6 +96,7 @@ const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, c
|
||||
.read_buffer = (binder_uintptr_t) (uintptr_t) readBuffer,
|
||||
};
|
||||
if (ioctl(binder->fd, BINDER_WRITE_READ, &exchange) != 0) {
|
||||
FF_DEBUG("BINDER_WRITE_READ(handle=%u, code=%u) failed: %s", handle, code, strerror(errno));
|
||||
return "BINDER_WRITE_READ failed";
|
||||
}
|
||||
|
||||
@@ -102,8 +114,10 @@ const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, c
|
||||
break;
|
||||
|
||||
case BR_FAILED_REPLY:
|
||||
FF_DEBUG("BR_FAILED_REPLY for handle=%u, code=%u", handle, code);
|
||||
return "Binder transaction failed";
|
||||
case BR_DEAD_REPLY:
|
||||
FF_DEBUG("BR_DEAD_REPLY: handle=%u is dead", handle);
|
||||
return "Binder service is not running";
|
||||
|
||||
case BR_INCREFS:
|
||||
@@ -122,6 +136,8 @@ const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, c
|
||||
|
||||
case BR_REPLY: {
|
||||
if ((size_t) (end - cursor) < sizeof(struct binder_transaction_data)) {
|
||||
FF_DEBUG("BR_REPLY is truncated: %zu of %zu bytes left",
|
||||
(size_t) (end - cursor), sizeof(struct binder_transaction_data));
|
||||
return "Truncated binder reply";
|
||||
}
|
||||
struct binder_transaction_data data;
|
||||
@@ -139,6 +155,8 @@ const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, c
|
||||
const char* error = nullptr;
|
||||
if (payloadSize > 0) {
|
||||
if (payloadSize > reply->capacity) {
|
||||
FF_DEBUG("The reply carries %zu payload bytes, the caller buffer holds %zu",
|
||||
payloadSize, reply->capacity);
|
||||
error = "Binder reply does not fit the caller buffer";
|
||||
} else {
|
||||
memcpy(reply->data, payload, payloadSize);
|
||||
@@ -146,8 +164,10 @@ const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, c
|
||||
}
|
||||
}
|
||||
|
||||
// Walk the offset table for the flat objects the service sent us and keep the
|
||||
// strong handles: those are the ones a later transaction can address.
|
||||
// Walk the offset table for the flat objects the service sent us. A reply may carry
|
||||
// both kinds at once and they are not interchangeable: a handle is a reference that
|
||||
// has to be acquired (see below) to outlive the reply buffer, while a file
|
||||
// descriptor was installed in our own fd table by the kernel and is simply ours.
|
||||
for (size_t offset = 0; offset + sizeof(binder_size_t) <= offsetsSize; offset += sizeof(binder_size_t)) {
|
||||
binder_size_t objectOffset = 0;
|
||||
memcpy(&objectOffset, offsets + offset, sizeof(objectOffset));
|
||||
@@ -156,11 +176,17 @@ const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, c
|
||||
}
|
||||
struct flat_binder_object object;
|
||||
memcpy(&object, payload + objectOffset, sizeof(object));
|
||||
if (object.hdr.type != BINDER_TYPE_HANDLE) {
|
||||
continue;
|
||||
}
|
||||
if (reply->handleCount < FF_BINDER_MAX_HANDLES) {
|
||||
reply->handles[reply->handleCount++] = object.handle;
|
||||
if (object.hdr.type == BINDER_TYPE_HANDLE) {
|
||||
if (reply->handleCount < FF_BINDER_MAX_HANDLES) {
|
||||
reply->handles[reply->handleCount++] = object.handle;
|
||||
}
|
||||
} else if (object.hdr.type == BINDER_TYPE_FD) {
|
||||
if (reply->fdCount < FF_BINDER_MAX_FDS) {
|
||||
reply->fds[reply->fdCount++] = (int32_t) object.handle;
|
||||
} else {
|
||||
// Nothing is going to hold this one and nothing is going to close it.
|
||||
close((int) object.handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,16 +223,26 @@ const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, c
|
||||
.read_consumed = 0,
|
||||
.read_buffer = 0,
|
||||
};
|
||||
ioctl(binder->fd, BINDER_WRITE_READ, &release);
|
||||
// This one ioctl carries the whole tail, so its failure is not cosmetic: without
|
||||
// BC_ACQUIRE the handles handed to the caller are only weakly referenced and every
|
||||
// later transaction on them fails with BR_FAILED_REPLY, which says nothing about the
|
||||
// real cause. Reported, but only when nothing more specific is already at hand.
|
||||
if (ioctl(binder->fd, BINDER_WRITE_READ, &release) != 0 && error == nullptr) {
|
||||
FF_DEBUG("BC_ACQUIRE/BC_FREE_BUFFER failed: %s", strerror(errno));
|
||||
error = "Releasing the binder reply failed";
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
default:
|
||||
FF_DEBUG("Unexpected binder command 0x%08x for handle=%u, code=%u", command, handle, code);
|
||||
return "Unexpected binder command";
|
||||
}
|
||||
}
|
||||
|
||||
FF_DEBUG("The binder reply for handle=%u, code=%u held no BR_REPLY (%zu bytes read)",
|
||||
handle, code, (size_t) exchange.read_consumed);
|
||||
return "No reply from binder";
|
||||
}
|
||||
|
||||
@@ -218,20 +254,81 @@ const char* ffBinderLookupService(FFBinder* binder, const char* name, uint32_t t
|
||||
|
||||
uint8_t replyBuffer[256];
|
||||
FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer));
|
||||
const char* error = ffBinderTransact(binder, FF_BINDER_SERVICE_MANAGER_HANDLE, transactionCode, &parcel, &reply);
|
||||
const char* error = ffBinderTransact(binder, FF_BINDER_SERVICE_MANAGER_HANDLE, transactionCode, 0, &parcel, &reply);
|
||||
if (error != nullptr) {
|
||||
return error;
|
||||
}
|
||||
if (ffBinderReplyIsStatus(&reply)) {
|
||||
FF_DEBUG("The service manager returned a status parcel for %s, code=%u", name, transactionCode);
|
||||
return "Service manager rejected the request";
|
||||
}
|
||||
if (ffBinderReadI32(reply.data, reply.size, 0) != 0) {
|
||||
const int32_t exception = ffBinderReadI32(reply.data, reply.size, 0);
|
||||
if (exception != 0) {
|
||||
FF_DEBUG("The service manager raised exception %d for %s", exception, name);
|
||||
return "Service manager raised an exception";
|
||||
}
|
||||
if (reply.handleCount == 0) {
|
||||
FF_DEBUG("The service manager returned no handle for %s", name);
|
||||
return "Service is not registered";
|
||||
}
|
||||
|
||||
*handle = reply.handles[0];
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ffBinderServiceHandleRelease(FFBinderServiceHandle* service) {
|
||||
if (service->binder == nullptr || service->handle == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The mirror of the acquire at the end of ffBinderTransact(): one strong and one weak reference
|
||||
// each, dropped in the same order they were taken. The kernel keeps the service node alive until
|
||||
// the last one goes, so this is what stops a short-lived fastfetch from holding one for the whole
|
||||
// of its life.
|
||||
const uint32_t commands[] = { BC_RELEASE, BC_DECREFS };
|
||||
uint8_t tail[2 * 2 * sizeof(uint32_t)];
|
||||
size_t tailSize = 0;
|
||||
for (size_t i = 0; i < 2; i++) {
|
||||
memcpy(tail + tailSize, &commands[i], sizeof(uint32_t));
|
||||
tailSize += sizeof(uint32_t);
|
||||
memcpy(tail + tailSize, &service->handle, sizeof(uint32_t));
|
||||
tailSize += sizeof(uint32_t);
|
||||
}
|
||||
|
||||
struct binder_write_read release = {
|
||||
.write_size = tailSize,
|
||||
.write_consumed = 0,
|
||||
.write_buffer = (binder_uintptr_t) (uintptr_t) tail,
|
||||
.read_size = 0,
|
||||
.read_consumed = 0,
|
||||
.read_buffer = 0,
|
||||
};
|
||||
// Not reported: this runs from a cleanup attribute, where there is no caller left to report to,
|
||||
// and the only way for it to fail is a binder that is already dead -- which every earlier call on
|
||||
// the same binder would have said already.
|
||||
ioctl(service->binder->fd, BINDER_WRITE_READ, &release);
|
||||
service->handle = 0;
|
||||
}
|
||||
|
||||
void ffBinderAcquiredRelease(FFBinderAcquired* acquired) {
|
||||
if (acquired->replied == nullptr || *acquired->replied == false) {
|
||||
return; // nothing was acquired
|
||||
}
|
||||
|
||||
// A descriptor is not a reference: the kernel installed it in our own fd table when it delivered
|
||||
// the reply, and nobody else holds it, so closing it is the whole of giving it back.
|
||||
for (uint32_t i = 0; i < acquired->fdCount; i++) {
|
||||
close(acquired->fds[i]);
|
||||
}
|
||||
|
||||
// The mirror of the acquire the transaction appended to its tail, in the same order it took them.
|
||||
// No binder to talk through means the handles went with it when it was closed.
|
||||
if (acquired->binder == nullptr || acquired->binder->fd < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < acquired->handleCount; i++) {
|
||||
FFBinderServiceHandle handle = { .binder = acquired->binder, .handle = acquired->handles[i] };
|
||||
ffBinderServiceHandleRelease(&handle);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
// * A handle arriving in a reply is owned by that reply buffer. It has to be acquired
|
||||
// (BC_ACQUIRE + BC_INCREFS) before BC_FREE_BUFFER; otherwise only a weak reference survives and
|
||||
// every later transaction on it fails, because the kernel looks up a strong reference.
|
||||
//
|
||||
// See .workbuddy-ai/android-binder-raw-client.md for the full write-up and the tooling.
|
||||
|
||||
#include "fastfetch.h" // IWYU pragma: keep
|
||||
|
||||
@@ -51,6 +49,12 @@
|
||||
// the surplus handles are left unacquired and will fail on first use.
|
||||
#define FF_BINDER_MAX_HANDLES 4
|
||||
|
||||
// How many file descriptors a reply may hand over. A descriptor is not a handle: the kernel installs
|
||||
// it in the caller's own fd table as it delivers the reply, so it is owned rather than borrowed and
|
||||
// the caller closes it. A reply carrying more than this still succeeds, and the surplus is closed
|
||||
// again by ffBinderTransact() because nothing would be left holding it.
|
||||
#define FF_BINDER_MAX_FDS 4
|
||||
|
||||
// The three int32 values Parcel::writeInterfaceToken() puts in front of the descriptor, and that
|
||||
// Parcel::enforceInterface() checks the last of. These come from libbinder rather than from the
|
||||
// kernel, which is why they are spelled out here.
|
||||
@@ -159,10 +163,12 @@ typedef struct FFBinderReply {
|
||||
uint32_t flags; // TF_* of the reply
|
||||
uint32_t handleCount;
|
||||
uint32_t handles[FF_BINDER_MAX_HANDLES];
|
||||
uint32_t fdCount;
|
||||
int32_t fds[FF_BINDER_MAX_FDS]; // owned by the caller, which closes them
|
||||
} FFBinderReply;
|
||||
|
||||
[[nodiscard]] static inline FFBinderReply ffBinderReplyCreate(uint8_t* data, size_t capacity) {
|
||||
return (FFBinderReply) { .data = data, .capacity = capacity, .size = 0, .code = 0, .flags = 0, .handleCount = 0, .handles = {} };
|
||||
return (FFBinderReply) { .data = data, .capacity = capacity, .size = 0, .code = 0, .flags = 0, .handleCount = 0, .handles = {}, .fdCount = 0, .fds = {} };
|
||||
}
|
||||
|
||||
// A reply carrying TF_STATUS_CODE is not an AIDL parcel at all: the first four bytes are the raw
|
||||
@@ -196,13 +202,21 @@ typedef struct FFBinderReply {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Synchronous transaction. Returns nullptr on success, a static message otherwise. Handles the
|
||||
// reply carries are acquired and added to `reply->handles` before the reply buffer is freed.
|
||||
// Synchronous transaction. Returns nullptr on success, a static message otherwise.
|
||||
//
|
||||
// `flags` carries the TF_* values for the request, and one of them is not cosmetic: a reply that
|
||||
// carries a file descriptor is only delivered at all when the request asked with TF_ACCEPT_FDS. A
|
||||
// request that did not gets BR_FAILED_REPLY instead, which reads like a service that is not running
|
||||
// rather than like a missing flag. TF_ONE_WAY is the other one the kernel acts on, and it is what
|
||||
// makes a call fire-and-forget -- there is then no reply to fill `reply` from.
|
||||
//
|
||||
// Handles the reply carries are acquired and added to `reply->handles` before the reply buffer is
|
||||
// freed; descriptors are added to `reply->fds`, which the caller owns and closes.
|
||||
//
|
||||
// `handle` 0 is the service manager; every other handle comes from ffBinderLookupService(). A
|
||||
// service that never replies blocks this call forever -- none of the services fastfetch reads does,
|
||||
// but there is no timeout here to fall back on.
|
||||
[[gnu::nonnull(1, 4, 5), nodiscard]] const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, const FFBinderParcel* parcel, FFBinderReply* reply);
|
||||
[[gnu::nonnull(1, 5, 6), nodiscard]] const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, uint32_t flags, const FFBinderParcel* parcel, FFBinderReply* reply);
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Service manager
|
||||
@@ -222,3 +236,42 @@ typedef struct FFBinderReply {
|
||||
|
||||
// Resolves a service name to a handle through the service manager. Returns nullptr on success.
|
||||
[[gnu::nonnull(1, 2, 4), nodiscard]] const char* ffBinderLookupService(FFBinder* binder, const char* name, uint32_t transactionCode, uint32_t* handle);
|
||||
|
||||
// A handle resolved through the service manager, held together with the binder it came from so that a
|
||||
// single cleanup attribute can give it back on every path out of the function that took it. Zeroed --
|
||||
// or left at handle 0 by a lookup that has not run or did not succeed -- it is a no-op, which is what
|
||||
// makes it safe to declare above the lookup itself.
|
||||
typedef struct FFBinderServiceHandle {
|
||||
FFBinder* binder;
|
||||
uint32_t handle;
|
||||
} FFBinderServiceHandle;
|
||||
|
||||
// Returns the strong and weak references ffBinderLookupService() acquired on the handle. Usable as a
|
||||
// cleanup attribute; safe on a zeroed or already released FFBinderServiceHandle.
|
||||
[[gnu::nonnull(1)]] void ffBinderServiceHandleRelease(FFBinderServiceHandle* service);
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Handles a transaction acquired
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
// Whatever ffBinderTransact() collected for the caller, released together on the way out of a
|
||||
// function -- the handles it acquired, plus the descriptors it delivered, which are owned rather
|
||||
// than borrowed and would otherwise stay open for the life of the process.
|
||||
//
|
||||
// Unlike FFBinderServiceHandle this cannot be declared before the transaction runs, because which
|
||||
// handles came back is only known afterwards. `replied` is therefore a separate flag the caller
|
||||
// raises once ffBinderTransact() has returned: it acquires the handles even when it goes on to fail
|
||||
// (a reply that did not fit the caller buffer still had its BC_ACQUIRE tail sent), so releasing
|
||||
// without it would drop a reference that was never taken.
|
||||
typedef struct FFBinderAcquired {
|
||||
FFBinder* binder;
|
||||
const uint32_t* handles;
|
||||
uint32_t handleCount;
|
||||
const int32_t* fds;
|
||||
uint32_t fdCount;
|
||||
const bool* replied;
|
||||
} FFBinderAcquired;
|
||||
|
||||
// Usable as a cleanup attribute; safe on a zeroed FFBinderAcquired, and on one whose transaction did
|
||||
// not reply.
|
||||
[[gnu::nonnull(1)]] void ffBinderAcquiredRelease(FFBinderAcquired* acquired);
|
||||
|
||||
+1001
-162
File diff suppressed because it is too large
Load Diff
+141
-4
@@ -24,7 +24,144 @@
|
||||
|
||||
#include "fastfetch.h"
|
||||
|
||||
// Resolves the int value of `<classDescriptor>.<fieldName>` out of the `classes.dex` of `jarPath`.
|
||||
// `classDescriptor` is the dex type descriptor, e.g. "Landroid/net/wifi/IWifiManager$Stub;".
|
||||
// Returns nullptr on success, a static message otherwise.
|
||||
[[gnu::nonnull(1, 2, 3, 4), nodiscard]] const char* ffDexStaticInt(const char* jarPath, const char* classDescriptor, const char* fieldName, int32_t* result);
|
||||
// What a request is written when the jar does not answer it, on every path out of ffDexStaticInts --
|
||||
// a jar that cannot be opened and a field the class does not declare both leave it behind, so a
|
||||
// caller reads a result rather than what its own stack happened to hold there. No transaction code
|
||||
// is negative: AIDL numbers them from `IBinder.FIRST_CALL_TRANSACTION`, which is 1.
|
||||
//
|
||||
// This is what keeps the reader a lookup and nothing more. Whether a code is one the caller cannot do
|
||||
// without or one it only uses when the build declares it, whether what came back is enough to carry
|
||||
// on, and whether the module reports a value or an error are the caller's decisions, made by reading
|
||||
// the results. The reader finds numbers and says which ones it could not.
|
||||
#define FF_DEX_STATIC_INT_UNRESOLVED (-1)
|
||||
|
||||
// One `<classDescriptor>.<fieldName>` lookup, and where its value is written. `classDescriptor` is
|
||||
// the dex type descriptor, e.g. "Landroid/net/wifi/IWifiManager$Stub;".
|
||||
typedef struct FFDexStaticIntRequest {
|
||||
const char* classDescriptor;
|
||||
const char* fieldName;
|
||||
int32_t* result;
|
||||
} FFDexStaticIntRequest;
|
||||
|
||||
// Resolves the int value of every request out of the dex entries of `jarPath`, walking `classes.dex`,
|
||||
// `classes2.dex`, ... in order until one of them defines the class.
|
||||
//
|
||||
// Asking for several fields at once is not a convenience. A lookup is dominated by the type table,
|
||||
// which lists every type the dex references -- 8428 descriptors in `framework.jar`'s `classes.dex`,
|
||||
// each one read from a different part of a 51 MB mapping -- and a single pass answers every request
|
||||
// whose class is named in it. Measured over three devices' own `framework.jar`, four fields of two
|
||||
// classes cost one pass rather than four.
|
||||
//
|
||||
// Every request is written a code or FF_DEX_STATIC_INT_UNRESOLVED. A request is never allowed to
|
||||
// take the others down with it: the classes a caller asks for are looked for independently, and one
|
||||
// the jar does not declare leaves its own result at the sentinel while the rest are answered.
|
||||
//
|
||||
// Not finding a code is not a failure. A jar that is readable answers the call, whether it declares
|
||||
// every field asked for, some of them, or none -- the results say which, and what that means for the
|
||||
// module is the caller's to decide. The return value is therefore reserved for the one thing the
|
||||
// caller cannot read out of the results: the jar itself, which is reported when it cannot be opened
|
||||
// or does not hold a dex that can be read. nullptr means the lookup ran.
|
||||
[[gnu::nonnull(1, 2), nodiscard]] const char* ffDexStaticInts(const char* jarPath, const FFDexStaticIntRequest* requests, uint32_t count);
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Reading a Parcelable's wire layout
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
//
|
||||
// A Parcelable's `writeToParcel` puts its fields on the wire in the order the method reads them, and
|
||||
// that order is the only thing that says where each one is. It is not in the class's declaration: a
|
||||
// dex stores a class's instance fields sorted by field index, which is sorted by *name*, so the
|
||||
// declaration order a reader would want is not recorded anywhere but the method body.
|
||||
//
|
||||
// Recovering it therefore means walking the bytecode. The compiler emits
|
||||
//
|
||||
// iget v0, p0, LDisplayInfo;->logicalWidth:I
|
||||
// invoke-virtual {p1, v0}, Landroid/os/Parcel;->writeInt(I)V
|
||||
//
|
||||
// once per field, in write order, so the `iget` opcodes in a linear pass over the instruction grid
|
||||
// are the sequence. Two things have to be got right for that pass to stay on the grid, and both are
|
||||
// easy to get wrong in a way that still produces a plausible-looking list:
|
||||
//
|
||||
// * Instructions are 1, 2, 3, 4 or 5 code units wide, and the width is a property of the opcode.
|
||||
// The table below is ART's `Instruction::SizeInCodeUnits`.
|
||||
// * The payload pseudo-instructions a `switch` or a `fill-array-data` leaves in the stream have a
|
||||
// low byte of zero and a high byte of 1, 2 or 3. They are not instructions, they sit behind every
|
||||
// reachable instruction, and stepping into one desynchronises the grid -- so the walk stops at the
|
||||
// first of them rather than trying to step over it.
|
||||
//
|
||||
// An array field is read twice: once for its `length`, which is the count that goes on the wire, and
|
||||
// again inside the loop that writes the elements. A class cannot declare two instance fields under
|
||||
// one name, so a repeat is always that reload, and the first occurrence is where the field is
|
||||
// written. The reader de-duplicates by name for that reason.
|
||||
//
|
||||
// What is *not* recovered is how each field is framed -- `writeString` against `writeString8`,
|
||||
// `writeParcelable` against `writeTypedObject`. That is a property of the method the field is handed
|
||||
// to, not of the field, and two `String` fields of the same class can differ: `DisplayInfo.name` goes
|
||||
// through `writeString8` while `DeviceProductInfo.mName` goes through `writeString`. Telling them
|
||||
// apart would mean resolving every invoke target, so the type descriptor is reported instead and the
|
||||
// caller frames the fields its descriptor settles -- an `I`, a `J`, an `[F` -- and names the few it
|
||||
// does not. Measured over three releases' own `framework.jar`, that is four names per class.
|
||||
//
|
||||
// The point of asking the device rather than carrying a table is that the field set moves: `DisplayInfo`
|
||||
// has 34 instance fields on Android 11, 42 on 13 and 59 on 16, and the fields that arrived in between
|
||||
// (`displayGroupId`, `renderFrameRate`, `appsSupportedModes`, `committedState`, ...) are exactly the
|
||||
// ones a parser written against one release reads out of the middle of another. Reading the sequence
|
||||
// from the build that is running makes every one of them optional by construction.
|
||||
|
||||
// A field name and a type descriptor are ASCII and short -- the longest name in `DisplayInfo` is 28
|
||||
// bytes and the longest descriptor `Landroid/hardware/display/DeviceProductInfo$ManufactureDate;` is
|
||||
// 60. A longer one is a class this reader was not written for, and is reported as unresolved rather
|
||||
// than truncated: half a name would silently match nothing, which is indistinguishable from the field
|
||||
// not being there.
|
||||
#define FF_DEX_PARCEL_FIELD_NAME_MAX 40
|
||||
#define FF_DEX_PARCEL_FIELD_TYPE_MAX 72
|
||||
|
||||
// One field of the write sequence. `type` is the dex type descriptor, e.g. "I", "[F",
|
||||
// "Ljava/lang/String;" or "Landroid/view/DisplayAddress;".
|
||||
typedef struct FFDexParcelField {
|
||||
char name[FF_DEX_PARCEL_FIELD_NAME_MAX];
|
||||
char type[FF_DEX_PARCEL_FIELD_TYPE_MAX];
|
||||
} FFDexParcelField;
|
||||
|
||||
// One class, and the two things that can be asked about it: the write sequence of one of its methods,
|
||||
// and which instance fields it declares. Either half may be left out, and both are answered out of
|
||||
// the same `class_data_item` -- which is why they are one request rather than two calls.
|
||||
//
|
||||
// Asking for them together is not a convenience. Both halves are read from a walk that is dominated
|
||||
// by the type table -- 8428 descriptors in `framework.jar`'s `classes.dex`, each read from a
|
||||
// different part of a 51 MB mapping -- and the fields a class declares are listed in the very
|
||||
// `class_data_item` the sequence is read out of. A second call would map the jar again, open every
|
||||
// entry again and make that pass again to reach a list the first call already had in hand. Measured
|
||||
// on a Redmi 9A, that second call was 7.0 ms of a 23.5 ms lookup.
|
||||
typedef struct FFDexParcelRequest {
|
||||
const char* classDescriptor;
|
||||
|
||||
// The write sequence. Left out by a request that only wants the instance fields, in which case
|
||||
// `fields`, `capacity` and `count` are not read.
|
||||
const char* methodName;
|
||||
FFDexParcelField* fields;
|
||||
uint32_t capacity;
|
||||
uint32_t* count; // how many fields the sequence holds, or 0 when the jar cannot answer
|
||||
|
||||
// The instance fields. This is the question a Parcelable whose layout is not field-driven needs.
|
||||
// `DisplayCutout` is one: its `writeCutoutToParcel` reads everything through `-$$Nest$` accessors
|
||||
// the compiler synthesised, so there is no `iget` to walk and no class name on the wire either.
|
||||
// What does say how long its body is, is which fields the class carries -- Android 11 declares
|
||||
// three, Android 13 adds `mCutoutPathParserInfo` and Android 16 adds `mSideOverrides`, and each
|
||||
// one adds a known run of writes behind the three both have. A declaration is a field set rather
|
||||
// than an order, which is exactly what this answers.
|
||||
const char* const* instanceFieldNames;
|
||||
bool* instanceFieldResults;
|
||||
uint32_t instanceFieldCount;
|
||||
} FFDexParcelRequest;
|
||||
|
||||
// Resolves every request out of the dex entries of `jarPath`, walking `classes.dex`, `classes2.dex`,
|
||||
// ... in order until one of them defines the class -- the same walk, and the same one pass over the
|
||||
// type table, that ffDexStaticInts makes.
|
||||
//
|
||||
// Every request is written its answers before anything can fail: a count of 0 for the sequence, and
|
||||
// `false` for every instance field. A class the jar does not declare, a method it does not declare,
|
||||
// and a sequence longer than the caller's `capacity` all leave the count at 0, and none of them holds
|
||||
// back the others: they are independent questions about the same pass. A count of 0 is not a failure
|
||||
// but an answer -- this build does not write that class, so a caller reading it has nothing to walk.
|
||||
// The return value is reserved for the jar itself, as in ffDexStaticInts.
|
||||
[[gnu::nonnull(1, 2), nodiscard]] const char* ffDexParcelFields(const char* jarPath, const FFDexParcelRequest* requests, uint32_t count);
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
#include "common/android/package.h"
|
||||
#include "common/android/api.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/strutil.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
// There is no way for a process to ask for its own package name: it is not in /proc/self/status, and
|
||||
// an app cannot list /data/data. The executable path carries it instead -- an app's binaries live
|
||||
// under /data/data/<package>/ or /data/user/<user>/<package>/, and /proc/self/exe resolves there.
|
||||
// Under Termux that is /data/data/com.termux/files/usr/bin/<binary>, whose third component is the
|
||||
// package.
|
||||
//
|
||||
// The memory cgroup carries the same information (4:memory:/apps/com.termux on the device this was
|
||||
// verified on), but the unified hierarchy spells it out as /apps/uid_10478/pid_26943 with no name
|
||||
// left in it, so the executable is the portable half of the two.
|
||||
bool ffAndroidGetOwnPackage(char* buffer, size_t capacity) {
|
||||
char path[4096];
|
||||
const ssize_t length = readlink("/proc/self/exe", path, sizeof(path) - 1);
|
||||
if (length <= 0) {
|
||||
FF_DEBUG("Cannot read /proc/self/exe, so the calling package cannot be derived");
|
||||
return false;
|
||||
}
|
||||
path[length] = '\0';
|
||||
|
||||
const char* rest = nullptr;
|
||||
const char* const dataPrefix = "/data/data/";
|
||||
const char* const userPrefix = "/data/user/";
|
||||
if (ffStrStartsWith(path, dataPrefix)) {
|
||||
rest = path + strlen(dataPrefix);
|
||||
} else if (ffStrStartsWith(path, userPrefix)) {
|
||||
// /data/user/<user>/<package>/... -- the user number has to be stepped over before the
|
||||
// package name starts, and only strchr() answers where that is.
|
||||
rest = strchr(path + strlen(userPrefix), '/');
|
||||
if (rest != nullptr) {
|
||||
rest += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (rest == nullptr) {
|
||||
const uint32_t uid = instance.state.platform.uid;
|
||||
FF_DEBUG("The executable is not in an app data directory (\"%s\"), uid %u", path, uid);
|
||||
if (uid != FF_ANDROID_PRIVILEGED_UID_SHELL) {
|
||||
return false;
|
||||
}
|
||||
static const char shellPackage[] = FF_ANDROID_SHELL_PACKAGE;
|
||||
if (sizeof(shellPackage) > capacity) {
|
||||
return false;
|
||||
}
|
||||
memcpy(buffer, shellPackage, sizeof(shellPackage));
|
||||
FF_DEBUG("The calling package is \"%s\" (the shell owns it)", buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* end = strchr(rest, '/');
|
||||
const size_t nameLength = end != nullptr ? (size_t) (end - rest) : strlen(rest);
|
||||
if (nameLength == 0 || nameLength >= capacity) {
|
||||
return false;
|
||||
}
|
||||
memcpy(buffer, rest, nameLength);
|
||||
buffer[nameLength] = '\0';
|
||||
FF_DEBUG("The calling package is \"%s\"", buffer);
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "fastfetch.h"
|
||||
|
||||
// The one package name the shell UID owns. android.os.Process.SHELL_UID is a single app UID that
|
||||
// owns exactly one package, so a process that cannot derive a package of its own -- a static build
|
||||
// pushed to /data/local/tmp is the case that showed up -- can answer with this name and have the
|
||||
// system services accept it. See FF_ANDROID_PRIVILEGED_UID_SHELL in common/android/api.h.
|
||||
#define FF_ANDROID_SHELL_PACKAGE "com.android.shell"
|
||||
|
||||
// Some of the system services take the caller's package name as an argument and check it against the
|
||||
// calling UID rather than against the name being real: a name nobody owns, and a real name belonging
|
||||
// to another app, are refused exactly like a null one. So it has to be the caller's own name.
|
||||
//
|
||||
// Whether the name is checked at all depends on the service and the release -- `getConnectionInfo`
|
||||
// answered "Package com.termux does not belong to <shell uid>" on one Android 16 device while the
|
||||
// same name was accepted on an Android 11 one, and `getWallpaper` refuses a null, empty or foreign
|
||||
// name on that Android 16 device -- so passing the real one is what works everywhere.
|
||||
//
|
||||
// Writes the name into `buffer`, terminator included, and returns whether one was found. `capacity`
|
||||
// is the caller's, because the parcel the name ends up in is sized from it.
|
||||
[[gnu::nonnull(1), nodiscard]] bool ffAndroidGetOwnPackage(char* buffer, size_t capacity);
|
||||
@@ -0,0 +1,100 @@
|
||||
#include "common/android/zip.h"
|
||||
#include "common/debug.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
static uint16_t zipU16(const uint8_t* p) {
|
||||
return (uint16_t) ((uint16_t) p[0] | (uint16_t) ((uint16_t) p[1] << 8));
|
||||
}
|
||||
|
||||
static uint32_t zipU32(const uint8_t* p) {
|
||||
return (uint32_t) p[0] | ((uint32_t) p[1] << 8) | ((uint32_t) p[2] << 16) | ((uint32_t) p[3] << 24);
|
||||
}
|
||||
|
||||
const char* ffAndroidZipFindEntry(const uint8_t* archive, size_t archiveSize, const char* entry, FFAndroidZipEntry* result) {
|
||||
const size_t entryLength = strlen(entry);
|
||||
if (archiveSize < FF_ZIP_EOCD_SIZE) {
|
||||
return "The archive is too small to be a zip";
|
||||
}
|
||||
|
||||
// The record is the last thing in the file, so it is searched for backwards -- over at most the
|
||||
// 64 KiB a trailing comment can take, not over the whole archive.
|
||||
const size_t newest = archiveSize - FF_ZIP_EOCD_SIZE;
|
||||
const size_t oldest = archiveSize > FF_ZIP_EOCD_MAX_COMMENT + FF_ZIP_EOCD_SIZE
|
||||
? archiveSize - FF_ZIP_EOCD_MAX_COMMENT - FF_ZIP_EOCD_SIZE
|
||||
: 0;
|
||||
size_t eocd = SIZE_MAX;
|
||||
for (size_t i = newest;; --i) {
|
||||
if (memcmp(archive + i, FF_ZIP_EOCD_MAGIC, sizeof(FF_ZIP_EOCD_MAGIC) - 1) == 0) {
|
||||
eocd = i;
|
||||
break;
|
||||
}
|
||||
if (i == oldest) {
|
||||
// The walk stops here rather than at zero: a `size_t` countdown that has to reach zero
|
||||
// before it stops would wrap and read the whole file backwards, which is the very scan
|
||||
// this is here to avoid.
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (eocd == SIZE_MAX) {
|
||||
FF_DEBUG("No end-of-central-directory record in the last %zu bytes of a %zu byte archive",
|
||||
newest - oldest + FF_ZIP_EOCD_SIZE, archiveSize);
|
||||
return "The archive has no zip directory";
|
||||
}
|
||||
|
||||
const uint32_t directorySize = zipU32(archive + eocd + FF_ZIP_OFF_EOCD_DIRECTORY_SIZE);
|
||||
const uint32_t directoryOffset = zipU32(archive + eocd + FF_ZIP_OFF_EOCD_DIRECTORY);
|
||||
if (directoryOffset > archiveSize || directorySize > archiveSize - directoryOffset) {
|
||||
return "The zip directory is out of range";
|
||||
}
|
||||
|
||||
// The directory is walked to its end rather than for the entry count the record carries: that
|
||||
// count is a 16-bit field which a 65535-entry archive pins at 0xFFFF and moves into a zip64
|
||||
// record, while the two bounds above describe the directory itself on every archive this small.
|
||||
const size_t directoryEnd = (size_t) directoryOffset + directorySize;
|
||||
for (size_t offset = directoryOffset; offset + FF_ZIP_CENTRAL_SIZE <= directoryEnd;) {
|
||||
if (memcmp(archive + offset, FF_ZIP_CENTRAL_MAGIC, sizeof(FF_ZIP_CENTRAL_MAGIC) - 1) != 0) {
|
||||
return "A zip directory entry is malformed";
|
||||
}
|
||||
const uint16_t nameLength = zipU16(archive + offset + FF_ZIP_OFF_CENTRAL_NAME_LENGTH);
|
||||
const uint16_t extraLength = zipU16(archive + offset + FF_ZIP_OFF_CENTRAL_EXTRA_LENGTH);
|
||||
const uint16_t commentLength = zipU16(archive + offset + FF_ZIP_OFF_CENTRAL_COMMENT_LENGTH);
|
||||
const size_t entrySize = FF_ZIP_CENTRAL_SIZE + (size_t) nameLength + (size_t) extraLength + (size_t) commentLength;
|
||||
if (entrySize > directoryEnd - offset) {
|
||||
FF_DEBUG("A zip entry claims %zu bytes but only %zu remain in the directory",
|
||||
entrySize, (size_t) (directoryEnd - offset));
|
||||
return "A zip directory entry runs past the directory";
|
||||
}
|
||||
|
||||
if (nameLength == entryLength && memcmp(archive + offset + FF_ZIP_CENTRAL_SIZE, entry, entryLength) == 0) {
|
||||
const uint32_t compressedSize = zipU32(archive + offset + FF_ZIP_OFF_CENTRAL_COMPRESSED_SIZE);
|
||||
const uint32_t uncompressedSize = zipU32(archive + offset + FF_ZIP_OFF_CENTRAL_UNCOMPRESSED_SIZE);
|
||||
const uint32_t localOffset = zipU32(archive + offset + FF_ZIP_OFF_CENTRAL_LOCAL_OFFSET);
|
||||
if (compressedSize == FF_ZIP_ZIP64_SENTINEL || uncompressedSize == FF_ZIP_ZIP64_SENTINEL || localOffset == FF_ZIP_ZIP64_SENTINEL) {
|
||||
FF_DEBUG("The zip entry \"%s\" is zip64: compressed=%u, uncompressed=%u, localOffset=%u",
|
||||
entry, compressedSize, uncompressedSize, localOffset);
|
||||
return "The zip entry is stored as zip64, which is not supported";
|
||||
}
|
||||
if (localOffset > archiveSize || FF_ZIP_LOCAL_HEADER_SIZE > archiveSize - localOffset) {
|
||||
return "A zip local header is out of range";
|
||||
}
|
||||
|
||||
const uint16_t localNameLength = zipU16(archive + localOffset + FF_ZIP_OFF_NAME_LENGTH);
|
||||
const uint16_t localExtraLength = zipU16(archive + localOffset + FF_ZIP_OFF_EXTRA_LENGTH);
|
||||
const size_t headerSize = FF_ZIP_LOCAL_HEADER_SIZE + (size_t) localNameLength + (size_t) localExtraLength;
|
||||
if (headerSize > archiveSize - localOffset || compressedSize > archiveSize - localOffset - headerSize) {
|
||||
return "The zip entry bounds are out of range";
|
||||
}
|
||||
|
||||
result->method = zipU16(archive + offset + FF_ZIP_OFF_CENTRAL_METHOD);
|
||||
result->uncompressedSize = uncompressedSize;
|
||||
result->payload = archive + localOffset + headerSize;
|
||||
result->payloadSize = compressedSize;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
offset += entrySize;
|
||||
}
|
||||
FF_DEBUG("No entry in the %zu byte archive is named \"%s\"", archiveSize, entry);
|
||||
return "The archive has no entry with that name";
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
|
||||
#include "fastfetch.h"
|
||||
|
||||
// Locating an entry inside a zip, which is what both the jar reader and the app label reader need:
|
||||
// one looks for `classesN.dex`, the other for the resource table of an APK.
|
||||
//
|
||||
// The entry is found through the central directory rather than by scanning for its local header,
|
||||
// and that is not a micro-optimisation. A local header is preceded by everything that came before
|
||||
// it, so scanning costs a pass over all of it, once per lookup: `framework.jar` is 52 MB and its
|
||||
// `classes2.dex` starts 9.6 MB in. Measured on the test device, where the sound module resolves
|
||||
// seven codes, looking the entries up by scanning was the difference between 30 ms and 2 seconds.
|
||||
//
|
||||
// The sizes and the compression method are read from the directory rather than from the entry's own
|
||||
// local header, which is the more reliable of the two: a local header is allowed to leave its sizes
|
||||
// at zero and put them in a data descriptor behind the payload, and an archive writer is free to
|
||||
// choose that. Only the local header's two length fields are used, because they are what says where
|
||||
// the payload begins -- the local extra field does not have to match the central one.
|
||||
|
||||
// The zip local file header, from APPNOTE.TXT 4.3.7.
|
||||
#define FF_ZIP_LOCAL_HEADER_SIZE 30
|
||||
#define FF_ZIP_OFF_NAME_LENGTH 26
|
||||
#define FF_ZIP_OFF_EXTRA_LENGTH 28
|
||||
|
||||
// The central directory.
|
||||
#define FF_ZIP_CENTRAL_MAGIC "PK\x01\x02"
|
||||
#define FF_ZIP_CENTRAL_SIZE 46
|
||||
#define FF_ZIP_OFF_CENTRAL_METHOD 10
|
||||
#define FF_ZIP_OFF_CENTRAL_COMPRESSED_SIZE 20
|
||||
#define FF_ZIP_OFF_CENTRAL_UNCOMPRESSED_SIZE 24
|
||||
#define FF_ZIP_OFF_CENTRAL_NAME_LENGTH 28
|
||||
#define FF_ZIP_OFF_CENTRAL_EXTRA_LENGTH 30
|
||||
#define FF_ZIP_OFF_CENTRAL_COMMENT_LENGTH 32
|
||||
#define FF_ZIP_OFF_CENTRAL_LOCAL_OFFSET 42
|
||||
|
||||
// The end of central directory record, which points at the directory. It is the last thing in the
|
||||
// file, behind a comment of at most 64 KiB, so it is searched for backwards over that much.
|
||||
#define FF_ZIP_EOCD_MAGIC "PK\x05\x06"
|
||||
#define FF_ZIP_EOCD_SIZE 22
|
||||
#define FF_ZIP_EOCD_MAX_COMMENT 0xffffu
|
||||
#define FF_ZIP_OFF_EOCD_DIRECTORY_SIZE 12
|
||||
#define FF_ZIP_OFF_EOCD_DIRECTORY 16
|
||||
|
||||
// A size or offset of 0xFFFFFFFF means the real one is in a zip64 extra field. AOSP's framework jars
|
||||
// and the APKs measured so far are orders of magnitude below that, and carrying the extra field
|
||||
// reader for them is not worth it.
|
||||
#define FF_ZIP_ZIP64_SENTINEL 0xffffffffu
|
||||
|
||||
#define FF_ZIP_METHOD_STORED 0
|
||||
#define FF_ZIP_METHOD_DEFLATED 8
|
||||
|
||||
// One entry, as the archive describes it. `payload` points into the mapping the caller passed in and
|
||||
// is not a copy: for a STORED entry it is the file's own bytes, and for a DEFLATED one it is the raw
|
||||
// deflate stream, which the caller inflates with `uncompressedSize` as the bound.
|
||||
typedef struct FFAndroidZipEntry {
|
||||
const uint8_t* payload;
|
||||
size_t payloadSize;
|
||||
uint32_t uncompressedSize;
|
||||
uint16_t method;
|
||||
} FFAndroidZipEntry;
|
||||
|
||||
// Resolves `entry` through the archive's central directory, writing where its payload is into
|
||||
// `result`. Returns nullptr on success, a static message otherwise -- an archive that is not a zip,
|
||||
// a directory that is out of range, an entry that is not there and an entry stored as zip64 are all
|
||||
// reported rather than answered with a pointer into nothing.
|
||||
[[gnu::nonnull(1, 3, 4), nodiscard]] const char* ffAndroidZipFindEntry(const uint8_t* archive, size_t archiveSize, const char* entry, FFAndroidZipEntry* result);
|
||||
@@ -3,7 +3,7 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>fastfetch</string>
|
||||
<string>@BUNDLE_ID@</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>@PROJECT_NAME@</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
|
||||
@@ -1,36 +1,47 @@
|
||||
#include "cf_helpers.h"
|
||||
#include "common/debug.h"
|
||||
|
||||
const char* ffCfNumGetInt64(CFTypeRef cf, int64_t* result) {
|
||||
if (CFGetTypeID(cf) == CFNumberGetTypeID()) {
|
||||
if (!CFNumberGetValue((CFNumberRef) cf, kCFNumberSInt64Type, result)) {
|
||||
FF_DEBUG("CFNumberGetValue(kCFNumberSInt64Type) failed, the number is of type 0x%08lx",
|
||||
(unsigned long) CFNumberGetType((CFNumberRef) cf));
|
||||
return "Number type is not SInt64";
|
||||
}
|
||||
return nullptr;
|
||||
} else if (CFGetTypeID(cf) == CFDataGetTypeID()) {
|
||||
if (CFDataGetLength((CFDataRef) cf) != sizeof(int64_t)) {
|
||||
FF_DEBUG("CFData holds %ld bytes, int64_t needs %zu",
|
||||
(long) CFDataGetLength((CFDataRef) cf), sizeof(int64_t));
|
||||
return "Data length is not sizeof(int64_t)";
|
||||
}
|
||||
CFDataGetBytes((CFDataRef) cf, CFRangeMake(0, sizeof(int64_t)), (uint8_t*) result);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FF_DEBUG("Expected a CFNumber or CFData, got TypeID 0x%08lx", (unsigned long) CFGetTypeID(cf));
|
||||
return "TypeID is neither 'CFNumber' nor 'CFData'";
|
||||
}
|
||||
|
||||
const char* ffCfNumGetInt(CFTypeRef cf, int32_t* result) {
|
||||
if (CFGetTypeID(cf) == CFNumberGetTypeID()) {
|
||||
if (!CFNumberGetValue((CFNumberRef) cf, kCFNumberSInt32Type, result)) {
|
||||
FF_DEBUG("CFNumberGetValue(kCFNumberSInt32Type) failed, the number is of type 0x%08lx",
|
||||
(unsigned long) CFNumberGetType((CFNumberRef) cf));
|
||||
return "Number type is not SInt32";
|
||||
}
|
||||
return nullptr;
|
||||
} else if (CFGetTypeID(cf) == CFDataGetTypeID()) {
|
||||
if (CFDataGetLength((CFDataRef) cf) != sizeof(*result)) {
|
||||
FF_DEBUG("CFData holds %ld bytes, int32_t needs %zu",
|
||||
(long) CFDataGetLength((CFDataRef) cf), sizeof(*result));
|
||||
return "Data length is not sizeof(int32_t)";
|
||||
}
|
||||
CFDataGetBytes((CFDataRef) cf, CFRangeMake(0, sizeof(*result)), (uint8_t*) result);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FF_DEBUG("Expected a CFNumber or CFData, got TypeID 0x%08lx", (unsigned long) CFGetTypeID(cf));
|
||||
return "TypeID is neither 'CFNumber' nor 'CFData'";
|
||||
}
|
||||
|
||||
@@ -38,16 +49,20 @@ const char* ffCfNumGetDouble(CFTypeRef cf, double* result) {
|
||||
if (CFGetTypeID(cf) == CFNumberGetTypeID()) {
|
||||
if (!CFNumberGetValue((CFNumberRef) cf, kCFNumberDoubleType, result) &&
|
||||
!CFNumberGetValue((CFNumberRef) cf, kCFNumberFloatType, result)) {
|
||||
FF_DEBUG("CFNumber is of type 0x%08lx, neither Double nor Float",
|
||||
(unsigned long) CFNumberGetType((CFNumberRef) cf));
|
||||
return "Number type is not Double or Float";
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FF_DEBUG("Expected a CFNumber, got TypeID 0x%08lx", (unsigned long) CFGetTypeID(cf));
|
||||
return "TypeID is neither 'CFNumber'";
|
||||
}
|
||||
|
||||
const char* ffCfDateGetEpoch(CFTypeRef cf, uint64_t* result) {
|
||||
if (CFGetTypeID(cf) != CFDateGetTypeID()) {
|
||||
FF_DEBUG("Expected a CFDate, got TypeID 0x%08lx", (unsigned long) CFGetTypeID(cf));
|
||||
return "TypeID is not 'CFDate'";
|
||||
}
|
||||
|
||||
@@ -76,6 +91,7 @@ const char* ffCfStrGetString(CFTypeRef cf, FFstrbuf* result) {
|
||||
}
|
||||
ffStrbufEnsureFixedLengthFree(result, (uint32_t) CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8));
|
||||
if (!CFStringGetCString(cfStr, result->chars, result->allocated, kCFStringEncodingUTF8)) {
|
||||
FF_DEBUG("CFStringGetCString() failed for a %u character string", (unsigned) length);
|
||||
return "CFStringGetCString() failed";
|
||||
}
|
||||
// CFStringGetCString ensures the buffer is NUL terminated
|
||||
@@ -93,6 +109,7 @@ const char* ffCfStrGetString(CFTypeRef cf, FFstrbuf* result) {
|
||||
result->length = (uint32_t) strnlen(result->chars, length);
|
||||
result->chars[result->length] = '\0';
|
||||
} else {
|
||||
FF_DEBUG("Expected a CFString or CFData, got TypeID 0x%08lx", (unsigned long) CFGetTypeID(cf));
|
||||
return "TypeID is neither 'CFString' nor 'CFData'";
|
||||
}
|
||||
|
||||
@@ -116,6 +133,7 @@ const char* ffCfDataGetDataAsString(CFTypeRef cf, FFstrbuf* result) {
|
||||
result->length = length;
|
||||
result->chars[result->length] = '\0';
|
||||
} else {
|
||||
FF_DEBUG("Expected a CFData, got TypeID 0x%08lx", (unsigned long) CFGetTypeID(cf));
|
||||
return "TypeID is not 'CFData'";
|
||||
}
|
||||
|
||||
@@ -125,6 +143,7 @@ const char* ffCfDataGetDataAsString(CFTypeRef cf, FFstrbuf* result) {
|
||||
const char* ffCfDictGetString(CFDictionaryRef dict, CFStringRef key, FFstrbuf* result) {
|
||||
CFTypeRef cf = (CFTypeRef) CFDictionaryGetValue(dict, key);
|
||||
if (cf == nullptr) {
|
||||
FF_DEBUG("The dictionary has no value for the key");
|
||||
return "CFDictionaryGetValue() failed";
|
||||
}
|
||||
|
||||
@@ -134,6 +153,7 @@ const char* ffCfDictGetString(CFDictionaryRef dict, CFStringRef key, FFstrbuf* r
|
||||
const char* ffCfDictGetDataAsString(CFDictionaryRef dict, CFStringRef key, FFstrbuf* result) {
|
||||
CFTypeRef cf = (CFTypeRef) CFDictionaryGetValue(dict, key);
|
||||
if (cf == nullptr) {
|
||||
FF_DEBUG("The dictionary has no value for the key");
|
||||
return "CFDictionaryGetValue() failed";
|
||||
}
|
||||
|
||||
@@ -143,10 +163,12 @@ const char* ffCfDictGetDataAsString(CFDictionaryRef dict, CFStringRef key, FFstr
|
||||
const char* ffCfDictGetBool(CFDictionaryRef dict, CFStringRef key, bool* result) {
|
||||
CFBooleanRef cf = (CFBooleanRef) CFDictionaryGetValue(dict, key);
|
||||
if (cf == nullptr) {
|
||||
FF_DEBUG("The dictionary has no value for the key");
|
||||
return "CFDictionaryGetValue() failed";
|
||||
}
|
||||
|
||||
if (CFGetTypeID(cf) != CFBooleanGetTypeID()) {
|
||||
FF_DEBUG("Expected a CFBoolean, got TypeID 0x%08lx", (unsigned long) CFGetTypeID(cf));
|
||||
return "TypeID is not 'CFBoolean'";
|
||||
}
|
||||
|
||||
@@ -157,6 +179,7 @@ const char* ffCfDictGetBool(CFDictionaryRef dict, CFStringRef key, bool* result)
|
||||
const char* ffCfDictGetInt(CFDictionaryRef dict, CFStringRef key, int* result) {
|
||||
CFTypeRef cf = (CFTypeRef) CFDictionaryGetValue(dict, key);
|
||||
if (cf == nullptr) {
|
||||
FF_DEBUG("The dictionary has no value for the key");
|
||||
return "CFDictionaryGetValue() failed";
|
||||
}
|
||||
|
||||
@@ -166,6 +189,7 @@ const char* ffCfDictGetInt(CFDictionaryRef dict, CFStringRef key, int* result) {
|
||||
const char* ffCfDictGetInt64(CFDictionaryRef dict, CFStringRef key, int64_t* result) {
|
||||
CFTypeRef cf = (CFTypeRef) CFDictionaryGetValue(dict, key);
|
||||
if (cf == nullptr) {
|
||||
FF_DEBUG("The dictionary has no value for the key");
|
||||
return "CFDictionaryGetValue() failed";
|
||||
}
|
||||
|
||||
@@ -175,6 +199,7 @@ const char* ffCfDictGetInt64(CFDictionaryRef dict, CFStringRef key, int64_t* res
|
||||
const char* ffCfDictGetDouble(CFDictionaryRef dict, CFStringRef key, double* result) {
|
||||
CFTypeRef cf = (CFTypeRef) CFDictionaryGetValue(dict, key);
|
||||
if (cf == nullptr) {
|
||||
FF_DEBUG("The dictionary has no value for the key");
|
||||
return "CFDictionaryGetValue() failed";
|
||||
}
|
||||
|
||||
@@ -184,16 +209,20 @@ const char* ffCfDictGetDouble(CFDictionaryRef dict, CFStringRef key, double* res
|
||||
const char* ffCfDictGetData(CFDictionaryRef dict, CFStringRef key, uint32_t offset, uint32_t size, uint8_t* result, uint32_t* length) {
|
||||
CFTypeRef cf = (CFTypeRef) CFDictionaryGetValue(dict, key);
|
||||
if (cf == nullptr) {
|
||||
FF_DEBUG("The dictionary has no value for the key");
|
||||
return "CFDictionaryGetValue() failed";
|
||||
}
|
||||
|
||||
if (CFGetTypeID(cf) != CFDataGetTypeID()) {
|
||||
FF_DEBUG("Expected a CFData, got TypeID 0x%08lx", (unsigned long) CFGetTypeID(cf));
|
||||
return "TypeID is not 'CFData'";
|
||||
}
|
||||
|
||||
CFIndex trueLength = CFDataGetLength((CFDataRef) cf);
|
||||
|
||||
if (trueLength < offset + size) {
|
||||
FF_DEBUG("The CFData holds %ld bytes, the caller asked for [%u, %u)",
|
||||
(long) trueLength, offset, offset + size);
|
||||
return "Data length is less than offset + size";
|
||||
}
|
||||
|
||||
@@ -208,6 +237,8 @@ const char* ffCfDictGetData(CFDictionaryRef dict, CFStringRef key, uint32_t offs
|
||||
const char* ffCfDictGetDict(CFDictionaryRef dict, CFStringRef key, CFDictionaryRef* result) {
|
||||
CFDictionaryRef cf = (CFDictionaryRef) CFDictionaryGetValue(dict, key);
|
||||
if (cf == nullptr || CFGetTypeID(cf) != CFDictionaryGetTypeID()) {
|
||||
FF_DEBUG("Expected a CFDictionary, got %s",
|
||||
cf == nullptr ? "no value at all" : "a value of another type");
|
||||
return "TypeID is not 'CFDictionary'";
|
||||
}
|
||||
|
||||
@@ -218,6 +249,7 @@ const char* ffCfDictGetDict(CFDictionaryRef dict, CFStringRef key, CFDictionaryR
|
||||
const char* ffCfDictGetDateAsEpoch(CFDictionaryRef dict, CFStringRef key, uint64_t* result) {
|
||||
CFTypeRef cf = (CFTypeRef) CFDictionaryGetValue(dict, key);
|
||||
if (cf == nullptr) {
|
||||
FF_DEBUG("The dictionary has no value for the key");
|
||||
return "CFDictionaryGetValue() failed";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "smc_temps.h"
|
||||
#include "common/apple/cf_helpers.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/strutil.h"
|
||||
|
||||
#include <stdint.h>
|
||||
@@ -83,7 +84,10 @@ static void smcUltostr(char* str, uint32_t val) {
|
||||
static const char* smcCall(io_connect_t conn, uint32_t selector, SmcKeyData_t* inputStructure, SmcKeyData_t* outputStructure) {
|
||||
size_t size = sizeof(SmcKeyData_t);
|
||||
|
||||
if (IOConnectCallStructMethod(conn, selector, inputStructure, size, outputStructure, &size) != kIOReturnSuccess) {
|
||||
const IOReturn callStatus = IOConnectCallStructMethod(conn, selector, inputStructure, size, outputStructure, &size);
|
||||
if (callStatus != kIOReturnSuccess) {
|
||||
FF_DEBUG("IOConnectCallStructMethod(selector=%u, command=%u) failed: IOReturn 0x%08x",
|
||||
selector, (uint32_t) (unsigned char) inputStructure->data8, (unsigned) callStatus);
|
||||
return "IOConnectCallStructMethod(conn) failed";
|
||||
}
|
||||
return nullptr;
|
||||
@@ -136,10 +140,13 @@ static const char* smcReadSmcVal(io_connect_t conn, const UInt32Char_t key, SmcV
|
||||
static const char* smcOpen(io_connect_t* conn) {
|
||||
FF_IOOBJECT_AUTO_RELEASE io_object_t device = IOServiceGetMatchingService(MACH_PORT_NULL, IOServiceMatching("AppleSMC"));
|
||||
if (!device) {
|
||||
FF_DEBUG("IOKit has no AppleSMC service");
|
||||
return "No SMC device found";
|
||||
}
|
||||
|
||||
if (IOServiceOpen(device, mach_task_self(), 0, conn) != kIOReturnSuccess) {
|
||||
const IOReturn openStatus = IOServiceOpen(device, mach_task_self(), 0, conn);
|
||||
if (openStatus != kIOReturnSuccess) {
|
||||
FF_DEBUG("IOServiceOpen(AppleSMC) failed: IOReturn 0x%08x", (unsigned) openStatus);
|
||||
return "IOServiceOpen() failed";
|
||||
}
|
||||
|
||||
@@ -153,6 +160,7 @@ static const char* smcReadValue(io_connect_t conn, const UInt32Char_t key, doubl
|
||||
return error;
|
||||
}
|
||||
if (val.dataSize == 0) {
|
||||
FF_DEBUG("The SMC key %s reported a data size of 0", key);
|
||||
return "Empty SMC result";
|
||||
}
|
||||
|
||||
@@ -173,9 +181,12 @@ static const char* smcReadValue(io_connect_t conn, const UInt32Char_t key, doubl
|
||||
*value = (double) ntohll(*(uint64_t*) (val.bytes));
|
||||
break;
|
||||
default:
|
||||
FF_DEBUG("SMC key %s: unsigned type %s has an unsupported size of %u bytes",
|
||||
key, val.dataType, val.dataSize);
|
||||
return "Unsupported SMC unsigned integer data size";
|
||||
}
|
||||
} else {
|
||||
FF_DEBUG("SMC key %s: unsupported unsigned type %s", key, val.dataType);
|
||||
return "Unsupported SMC unsigned data type";
|
||||
}
|
||||
break;
|
||||
@@ -204,9 +215,13 @@ static const char* smcReadValue(io_connect_t conn, const UInt32Char_t key, doubl
|
||||
} else if (ffStrEquals(val.dataType, "fpe2")) {
|
||||
*value = ntohs(*(uint16_t*) (val.bytes)) / 4.0;
|
||||
} else {
|
||||
FF_DEBUG("SMC key %s: unsupported fixed point type %s of %u bytes",
|
||||
key, val.dataType, val.dataSize);
|
||||
return "Unsupported SMC floating point data type";
|
||||
}
|
||||
} else {
|
||||
FF_DEBUG("SMC key %s: unsupported floating point type %s of %u bytes",
|
||||
key, val.dataType, val.dataSize);
|
||||
return "Unsupported SMC floating point data type";
|
||||
}
|
||||
break;
|
||||
@@ -227,6 +242,8 @@ static const char* smcReadValue(io_connect_t conn, const UInt32Char_t key, doubl
|
||||
*value = (double) ntohll(*(int64_t*) (val.bytes));
|
||||
break;
|
||||
default:
|
||||
FF_DEBUG("SMC key %s: signed type %s has an unsupported size of %u bytes",
|
||||
key, val.dataType, val.dataSize);
|
||||
return "Unsupported SMC signed integer data size";
|
||||
}
|
||||
} else if (val.dataType[1] == 'p' && val.dataSize == 2) // signed fixed point types
|
||||
@@ -252,9 +269,12 @@ static const char* smcReadValue(io_connect_t conn, const UInt32Char_t key, doubl
|
||||
} else if (ffStrEquals(val.dataType, "spf0")) {
|
||||
*value = (int16_t) ntohs(*(int16_t*) (val.bytes)) / 1.0;
|
||||
} else {
|
||||
FF_DEBUG("SMC key %s: unsupported signed fixed point type %s of %u bytes",
|
||||
key, val.dataType, val.dataSize);
|
||||
return "Unsupported SMC signed integer data type";
|
||||
}
|
||||
} else {
|
||||
FF_DEBUG("SMC key %s: unsupported signed type %s", key, val.dataType);
|
||||
return "Unsupported SMC signed data type";
|
||||
}
|
||||
break;
|
||||
@@ -263,11 +283,15 @@ static const char* smcReadValue(io_connect_t conn, const UInt32Char_t key, doubl
|
||||
if (ffStrEquals(val.dataType, "{pwm") && val.dataSize == 2) {
|
||||
*value = (double) ntohs(*(uint16_t*) (val.bytes)) * 100 / 65536.0;
|
||||
} else {
|
||||
FF_DEBUG("SMC key %s: unsupported special type %s of %u bytes", key, val.dataType, val.dataSize);
|
||||
return "Unsupported SMC special data type";
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
FF_DEBUG("SMC key %s: unknown data type '%c%c' (%s) of %u bytes",
|
||||
key, val.dataType[0] ? val.dataType[0] : '?', val.dataType[1] ? val.dataType[1] : '?',
|
||||
val.dataType, val.dataSize);
|
||||
return "Unsupported SMC data type";
|
||||
}
|
||||
return nullptr;
|
||||
@@ -300,6 +324,7 @@ const char* ffDetectSmcSpecificTemp(const char* sensor, double* result) {
|
||||
}
|
||||
|
||||
if (!detectTemp(conn, sensor, result)) {
|
||||
FF_DEBUG("The SMC sensor %s could not be read, or read a value outside 10..120", sensor);
|
||||
return "Could not read SMC temperature";
|
||||
}
|
||||
|
||||
@@ -490,6 +515,7 @@ const char* ffDetectSmcTemps(enum FFTempType type, double* result) {
|
||||
break;
|
||||
}
|
||||
|
||||
FF_DEBUG("None of the SMC sensors for temp type %d reported a usable reading", (int) type);
|
||||
if (count == 0) {
|
||||
return "No temperatures detected";
|
||||
}
|
||||
|
||||
+12
-28
@@ -1,38 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "fastfetch.h"
|
||||
#include "common/time.h"
|
||||
|
||||
[[gnu::nonnull(1), gnu::pure, nodiscard]] static inline const char* ffFindFileName(const char* file) {
|
||||
const char* lastSlash = __builtin_strrchr(file, '/');
|
||||
#ifdef _WIN32
|
||||
if (lastSlash == nullptr) {
|
||||
lastSlash = __builtin_strrchr(file, '\\');
|
||||
}
|
||||
#endif
|
||||
if (lastSlash != nullptr) {
|
||||
return lastSlash + 1;
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
#ifndef NDEBUG
|
||||
#define FF_DEBUG_PRINT(file_, line_, format_, ...) \
|
||||
do { \
|
||||
if (instance.config.display.debugMode) \
|
||||
fprintf(stderr, "[%s%4d, %s] " format_ "\n", ffFindFileName(file_), line_, ffTimeToTimeStr(ffTimeGetNow()), ##__VA_ARGS__); \
|
||||
} while (0)
|
||||
#else
|
||||
#define FF_DEBUG_PRINT(file_, line_, format_, ...) \
|
||||
do { \
|
||||
} while (0)
|
||||
#endif
|
||||
#include "fastfetch.h"
|
||||
#include "common/time.h"
|
||||
|
||||
#define FF_DEBUG(format, ...) FF_DEBUG_PRINT(__FILE__, __LINE__, format, ##__VA_ARGS__)
|
||||
[[gnu::nonnull(1, 3), gnu::format(printf, 3, 4)]]
|
||||
void ffDebugPrint(const char* file, int line, const char* format, ...);
|
||||
|
||||
#if _WIN32
|
||||
#define FF_DEBUG(format, ...) ffDebugPrint(__FILE__, __LINE__, format, ##__VA_ARGS__)
|
||||
|
||||
#if _WIN32
|
||||
const char* ffDebugWin32Error(DWORD errorCode);
|
||||
const char* ffDebugNtStatus(NTSTATUS status);
|
||||
const char* ffDebugConfigRet(unsigned long /*CONFIGRET*/ ret);
|
||||
const char* ffDebugHResult(HRESULT hr);
|
||||
#endif
|
||||
|
||||
#else
|
||||
|
||||
#define FF_DEBUG(format, ...) ((void) 0)
|
||||
#endif
|
||||
|
||||
@@ -64,7 +64,11 @@ static void getExePath(FFPlatform* platform) {
|
||||
exePathLen--; // remove terminating NUL
|
||||
}
|
||||
#elif defined(__OpenBSD__)
|
||||
// OpenBSD doesn't have a reliable way to get the executable path.
|
||||
#if FF_HAVE_GETEXECPATH
|
||||
// https://man.openbsd.org/getexecpath.3
|
||||
size_t exePathLen = getexecpath(exePath, sizeof(exePath)) == 0 ? strlen(exePath) : 0;
|
||||
#else
|
||||
// OpenBSD (before 8.0) doesn't have a reliable way to get the executable path.
|
||||
// Current implementation uses argv[0], which can be easily spoofed.
|
||||
// See #2195
|
||||
size_t exePathLen = 0;
|
||||
@@ -165,6 +169,7 @@ static void getExePath(FFPlatform* platform) {
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#elif defined(__sun)
|
||||
ssize_t exePathLen = readlink("/proc/self/path/a.out", exePath, sizeof(exePath) - 1);
|
||||
if (exePathLen > 0) {
|
||||
@@ -299,10 +304,7 @@ static void getHostName(FFPlatform* platform, const struct utsname* uts) {
|
||||
}
|
||||
|
||||
static void getUserShell(FFPlatform* platform, const struct passwd* pwd) {
|
||||
const char* shell = getenv("SHELL");
|
||||
if (!ffStrSet(shell) && pwd) {
|
||||
shell = pwd->pw_shell;
|
||||
}
|
||||
const char* shell = pwd ? pwd->pw_shell : getenv("SHELL");
|
||||
|
||||
ffStrbufAppendS(&platform->userShell, shell);
|
||||
}
|
||||
@@ -336,7 +338,12 @@ static void getCwd(FFPlatform* platform) {
|
||||
void ffPlatformInitImpl(FFPlatform* platform) {
|
||||
platform->pid = (uint32_t) getpid();
|
||||
platform->uid = getuid();
|
||||
#if !__ANDROID__
|
||||
struct passwd* pwd = getpwuid(platform->uid);
|
||||
#else
|
||||
// On Android, /etc/passwd is empty, and getpwuid() will return `/data` for pw_dir
|
||||
struct passwd* pwd = nullptr;
|
||||
#endif
|
||||
|
||||
struct utsname uts;
|
||||
if (uname(&uts) < 0) {
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
#include "common/io.h"
|
||||
#include "common/library.h"
|
||||
#include "common/strutil.h"
|
||||
#include "common/windows/folders.h"
|
||||
#include "common/windows/unicode.h"
|
||||
#include "common/windows/registry.h"
|
||||
#include "common/windows/nt.h"
|
||||
|
||||
#include <stdalign.h>
|
||||
#include <windows.h>
|
||||
#include <shlobj.h>
|
||||
#include <sddl.h>
|
||||
|
||||
#define SECURITY_WIN32 1 // For secext.h
|
||||
@@ -43,43 +43,27 @@ static void getExePath(FFPlatform* platform) {
|
||||
ffStrbufReplaceAllC(&platform->exePath, '\\', '/');
|
||||
}
|
||||
|
||||
static void getHomeDir(FFPlatform* platform) {
|
||||
PWSTR pPath = nullptr;
|
||||
if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_Profile, KF_FLAG_DEFAULT, nullptr, &pPath))) {
|
||||
ffStrbufSetWS(&platform->homeDir, pPath);
|
||||
ffStrbufReplaceAllC(&platform->homeDir, '\\', '/');
|
||||
ffStrbufEnsureEndsWithC(&platform->homeDir, '/');
|
||||
} else {
|
||||
ffStrbufSetS(&platform->homeDir, getenv("USERPROFILE"));
|
||||
ffStrbufReplaceAllC(&platform->homeDir, '\\', '/');
|
||||
ffStrbufEnsureEndsWithC(&platform->homeDir, '/');
|
||||
}
|
||||
CoTaskMemFree(pPath);
|
||||
}
|
||||
|
||||
static void getCacheDir(FFPlatform* platform) {
|
||||
PWSTR pPath = nullptr;
|
||||
if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_LocalAppData, KF_FLAG_DEFAULT, nullptr, &pPath))) {
|
||||
ffStrbufSetWS(&platform->cacheDir, pPath);
|
||||
ffStrbufReplaceAllC(&platform->cacheDir, '\\', '/');
|
||||
ffStrbufEnsureEndsWithC(&platform->cacheDir, '/');
|
||||
} else {
|
||||
if (!ffGetKnownFolderPath(FF_KNOWN_FOLDER_LOCAL_APP_DATA, &platform->cacheDir)) {
|
||||
ffStrbufAppend(&platform->cacheDir, &platform->homeDir);
|
||||
ffStrbufAppendS(&platform->cacheDir, "AppData/Local/");
|
||||
return;
|
||||
}
|
||||
CoTaskMemFree(pPath);
|
||||
|
||||
ffStrbufReplaceAllC(&platform->cacheDir, '\\', '/');
|
||||
ffStrbufEnsureEndsWithC(&platform->cacheDir, '/');
|
||||
}
|
||||
|
||||
static void platformPathAddKnownFolder(FFlist* dirs, REFKNOWNFOLDERID folderId) {
|
||||
PWSTR pPath = nullptr;
|
||||
if (SUCCEEDED(SHGetKnownFolderPath(folderId, KF_FLAG_DEFAULT, nullptr, &pPath))) {
|
||||
FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreateWS(pPath);
|
||||
CoTaskMemFree(pPath);
|
||||
ffStrbufReplaceAllC(&buffer, '\\', '/');
|
||||
ffStrbufEnsureEndsWithC(&buffer, '/');
|
||||
if (!FF_LIST_CONTAINS(*dirs, &buffer, ffStrbufEqual)) {
|
||||
ffStrbufInitMove(FF_LIST_ADD(FFstrbuf, *dirs), &buffer);
|
||||
}
|
||||
static void platformPathAddKnownFolder(FFlist* dirs, FFKnownFolder folder) {
|
||||
FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
|
||||
if (!ffGetKnownFolderPath(folder, &buffer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ffStrbufReplaceAllC(&buffer, '\\', '/');
|
||||
ffStrbufEnsureEndsWithC(&buffer, '/');
|
||||
if (!FF_LIST_CONTAINS(*dirs, &buffer, ffStrbufEqual)) {
|
||||
ffStrbufInitMove(FF_LIST_ADD(FFstrbuf, *dirs), &buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,9 +96,9 @@ static void getConfigDirs(FFPlatform* platform) {
|
||||
}
|
||||
|
||||
ffPlatformPathAddHome(&platform->configDirs, platform, ".config/");
|
||||
platformPathAddKnownFolder(&platform->configDirs, &FOLDERID_ProgramData);
|
||||
platformPathAddKnownFolder(&platform->configDirs, &FOLDERID_RoamingAppData);
|
||||
platformPathAddKnownFolder(&platform->configDirs, &FOLDERID_LocalAppData);
|
||||
platformPathAddKnownFolder(&platform->configDirs, FF_KNOWN_FOLDER_PROGRAM_DATA);
|
||||
platformPathAddKnownFolder(&platform->configDirs, FF_KNOWN_FOLDER_ROAMING_APP_DATA);
|
||||
platformPathAddKnownFolder(&platform->configDirs, FF_KNOWN_FOLDER_LOCAL_APP_DATA);
|
||||
ffPlatformPathAddHome(&platform->configDirs, platform, "");
|
||||
}
|
||||
|
||||
@@ -126,9 +110,9 @@ static void getDataDirs(FFPlatform* platform) {
|
||||
platformPathAddEnvSuffix(&platform->dataDirs, "MINGW_PREFIX", "share");
|
||||
}
|
||||
ffPlatformPathAddHome(&platform->dataDirs, platform, ".local/share/");
|
||||
platformPathAddKnownFolder(&platform->dataDirs, &FOLDERID_ProgramData);
|
||||
platformPathAddKnownFolder(&platform->dataDirs, &FOLDERID_RoamingAppData);
|
||||
platformPathAddKnownFolder(&platform->dataDirs, &FOLDERID_LocalAppData);
|
||||
platformPathAddKnownFolder(&platform->dataDirs, FF_KNOWN_FOLDER_PROGRAM_DATA);
|
||||
platformPathAddKnownFolder(&platform->dataDirs, FF_KNOWN_FOLDER_ROAMING_APP_DATA);
|
||||
platformPathAddKnownFolder(&platform->dataDirs, FF_KNOWN_FOLDER_LOCAL_APP_DATA);
|
||||
ffPlatformPathAddHome(&platform->dataDirs, platform, "");
|
||||
}
|
||||
|
||||
@@ -156,9 +140,25 @@ static void getUserName(FFPlatform* platform) {
|
||||
TOKEN_USER* tokenUser = (TOKEN_USER*) buf;
|
||||
UNICODE_STRING sidString = { .Buffer = buffer, .Length = 0, .MaximumLength = sizeof(buffer) };
|
||||
if (NT_SUCCESS(RtlConvertSidToUnicodeString(&sidString, tokenUser->User.Sid, FALSE))) {
|
||||
ffStrbufSetNWS(&platform->sid, sidString.Length / sizeof(wchar_t), sidString.Buffer);
|
||||
uint32_t sidLength = sidString.Length / sizeof(wchar_t);
|
||||
ffStrbufSetNWS(&platform->sid, sidLength, sidString.Buffer);
|
||||
|
||||
// The SID is already in hand, so the profile directory costs one registry read rather
|
||||
// than a second NtQueryInformationToken(). ffGetProfilePath() needs a null-terminated
|
||||
// string, which RtlConvertSidToUnicodeString() does not produce.
|
||||
if (sidLength < ARRAY_SIZE(buffer)) {
|
||||
buffer[sidLength] = L'\0';
|
||||
ffGetProfilePath(buffer, &platform->homeDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (platform->homeDir.length == 0) {
|
||||
ffStrbufSetS(&platform->homeDir, getenv("USERPROFILE")); // Last resort; ffGetProfilePath() covers the normal case
|
||||
}
|
||||
|
||||
ffStrbufReplaceAllC(&platform->homeDir, '\\', '/');
|
||||
ffStrbufEnsureEndsWithC(&platform->homeDir, '/');
|
||||
}
|
||||
|
||||
static void getHostName(FFPlatform* platform) {
|
||||
@@ -280,12 +280,12 @@ void ffPlatformInitImpl(FFPlatform* platform) {
|
||||
platform->pid = (uint32_t) (uintptr_t) ffGetTeb()->ClientId.UniqueProcess;
|
||||
getExePath(platform);
|
||||
getCwd(platform);
|
||||
getHomeDir(platform);
|
||||
|
||||
getUserName(platform);
|
||||
|
||||
getCacheDir(platform);
|
||||
getConfigDirs(platform);
|
||||
getDataDirs(platform);
|
||||
|
||||
getUserName(platform);
|
||||
getHostName(platform);
|
||||
getUserShell(platform);
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#include "common/FFcache.h"
|
||||
|
||||
// Entries are file scope statics owned by the cache owner, so they can be linked into a list
|
||||
// without any allocation. The list holds every entry that has been used at least once: an entry
|
||||
// that was never used has nothing to drop.
|
||||
static FFcacheEntry* entries = nullptr;
|
||||
|
||||
static void registerEntry(FFcacheEntry* entry) {
|
||||
if (entry->registered) {
|
||||
return;
|
||||
}
|
||||
|
||||
entry->registered = true;
|
||||
entry->next = entries;
|
||||
entries = entry;
|
||||
}
|
||||
|
||||
void* ffCacheGet(FFcacheEntry* entry) {
|
||||
if (ffCacheBeginInit(entry) && entry->init) {
|
||||
entry->init(entry->storage);
|
||||
}
|
||||
|
||||
return entry->storage;
|
||||
}
|
||||
|
||||
bool ffCacheBeginInit(FFcacheEntry* entry) {
|
||||
registerEntry(entry);
|
||||
|
||||
if (entry->initialized) {
|
||||
return false;
|
||||
}
|
||||
|
||||
entry->initialized = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ffCacheInvalidate(FFcacheEntry* entry) {
|
||||
if (!entry->initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
entry->initialized = false;
|
||||
|
||||
if (entry->destroy) {
|
||||
entry->destroy(entry->storage);
|
||||
}
|
||||
}
|
||||
|
||||
void ffCacheInvalidateAll(void) {
|
||||
for (FFcacheEntry* entry = entries; entry; entry = entry->next) {
|
||||
ffCacheInvalidate(entry);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
#include "common/binary.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/io.h"
|
||||
#include "common/strutil.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
@@ -108,6 +110,7 @@ static const char* dumpMachHeader(const FFMemoryMapping* mapping, off_t offset,
|
||||
if (is_64) {
|
||||
const struct mach_header_64* header = readData(mapping, sizeof(struct mach_header_64), offset);
|
||||
if (!header) {
|
||||
FF_DEBUG("Failed to read the 64-bit mach header at offset %lld", (long long) offset);
|
||||
return "read mach header failed";
|
||||
}
|
||||
|
||||
@@ -116,6 +119,7 @@ static const char* dumpMachHeader(const FFMemoryMapping* mapping, off_t offset,
|
||||
} else {
|
||||
const struct mach_header* header = readData(mapping, sizeof(struct mach_header), offset);
|
||||
if (!header) {
|
||||
FF_DEBUG("Failed to read the mach header at offset %lld", (long long) offset);
|
||||
return "read mach header failed";
|
||||
}
|
||||
|
||||
@@ -200,6 +204,7 @@ static const char* dumpMachHeader(const FFMemoryMapping* mapping, off_t offset,
|
||||
static const char* dumpFatHeader(const FFMemoryMapping* mapping, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) {
|
||||
const struct fat_header* headerRaw = readData(mapping, sizeof(struct fat_header), 0);
|
||||
if (!headerRaw) {
|
||||
FF_DEBUG("Failed to read the fat header");
|
||||
return "read fat header failed";
|
||||
}
|
||||
|
||||
@@ -251,6 +256,7 @@ static const char* dumpFatHeader(const FFMemoryMapping* mapping, bool (*cb)(cons
|
||||
return dumpMachHeader(mapping, machHeaderOffset, *magic == MH_MAGIC_64, cb, userdata, minLength);
|
||||
}
|
||||
}
|
||||
FF_DEBUG("No supported Mach-O slice was found in the fat binary");
|
||||
return "Unsupported fat header";
|
||||
}
|
||||
|
||||
@@ -265,11 +271,13 @@ static const char* dumpFatHeader(const FFMemoryMapping* mapping, bool (*cb)(cons
|
||||
const char* ffBinaryExtractStrings(const char* machoFile, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) {
|
||||
FF_AUTO_CLOSE_FD int fd = open(machoFile, O_RDONLY | O_CLOEXEC);
|
||||
if (fd < 0) {
|
||||
FF_DEBUG("open(\"%s\") failed: %s", machoFile, strerror(errno));
|
||||
return "File could not be opened";
|
||||
}
|
||||
|
||||
struct stat st;
|
||||
if (fstat(fd, &st) != 0 || st.st_size <= 0) {
|
||||
FF_DEBUG("fstat(\"%s\") failed or the file is empty", machoFile);
|
||||
return "Failed to stat file";
|
||||
}
|
||||
|
||||
@@ -278,12 +286,14 @@ const char* ffBinaryExtractStrings(const char* machoFile, bool (*cb)(const char*
|
||||
.length = (size_t) st.st_size,
|
||||
};
|
||||
if (mapping.data == MAP_FAILED) {
|
||||
FF_DEBUG("mmap(\"%s\", %lld bytes) failed: %s", machoFile, (long long) st.st_size, strerror(errno));
|
||||
return "mmap failed";
|
||||
}
|
||||
|
||||
// Read the magic number to determine the type of binary
|
||||
const uint32_t* magic = readData(&mapping, sizeof(uint32_t), 0);
|
||||
if (!magic) {
|
||||
FF_DEBUG("Failed to read the magic number");
|
||||
return "read magic number failed";
|
||||
}
|
||||
|
||||
@@ -291,6 +301,7 @@ const char* ffBinaryExtractStrings(const char* machoFile, bool (*cb)(const char*
|
||||
// MH_CIGAM and MH_CIGAM_64 seem to be no longer used, as `swap_mach_header` is marked as deprecated.
|
||||
// However FAT_CIGAM and FAT_CIGAM_64 are still used (/usr/bin/vim).
|
||||
if (*magic != MH_MAGIC && *magic != MH_MAGIC_64 && *magic != FAT_CIGAM && *magic != FAT_CIGAM_64 && *magic != FAT_MAGIC && *magic != FAT_MAGIC_64) {
|
||||
FF_DEBUG("Unsupported magic number 0x%08x", (unsigned) *magic);
|
||||
return "Unsupported format or big endian mach-o file";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "common/binary.h"
|
||||
#include "common/debug.h"
|
||||
|
||||
#if defined(FF_HAVE_ELF) || defined(__sun) || (defined(__FreeBSD__) && !defined(__DragonFly__)) || defined(__OpenBSD__) || defined(__NetBSD__)
|
||||
|
||||
@@ -43,6 +44,7 @@ const char* ffBinaryExtractStrings(const char* elfFile, bool (*cb)(const char* s
|
||||
FF_LIBRARY_LOAD_MESSAGE(libelf, "libelf" FF_LIBRARY_EXTENSION, 1);
|
||||
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libelf, elfData, elf_version)
|
||||
if (elfData.ffelf_version(EV_CURRENT) == EV_NONE) {
|
||||
FF_DEBUG("elf_version() failed");
|
||||
return "elf_version() failed";
|
||||
}
|
||||
|
||||
@@ -60,23 +62,27 @@ const char* ffBinaryExtractStrings(const char* elfFile, bool (*cb)(const char* s
|
||||
}
|
||||
|
||||
if (elfData.ffelf_end == nullptr) {
|
||||
FF_DEBUG("load libelf failed");
|
||||
return "load libelf failed";
|
||||
}
|
||||
|
||||
// Open the ELF file
|
||||
FF_AUTO_CLOSE_FD int fd = open(elfFile, O_RDONLY | O_CLOEXEC);
|
||||
if (fd < 0) {
|
||||
FF_DEBUG("open(%s) failed: %s", elfFile, strerror(errno));
|
||||
return "open() failed";
|
||||
}
|
||||
|
||||
Elf* elf = elfData.ffelf_begin(fd, ELF_C_READ, nullptr);
|
||||
if (elf == nullptr) {
|
||||
FF_DEBUG("elf_begin() failed");
|
||||
return "elf_begin() failed";
|
||||
}
|
||||
|
||||
// Get the section header string table index
|
||||
size_t shstrndx = 0;
|
||||
if (elfData.ffelf_getshdrstrndx(elf, &shstrndx) < 0) {
|
||||
FF_DEBUG("elf_getshdrstrndx() failed");
|
||||
elfData.ffelf_end(elf);
|
||||
return "elf_getshdrstrndx() failed";
|
||||
}
|
||||
@@ -141,6 +147,7 @@ const char* ffBinaryExtractStrings(const char* elfFile, bool (*cb)(const char* s
|
||||
*/
|
||||
const char* ffBinaryExtractStrings(const char* file, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) {
|
||||
FF_UNUSED(file, cb, userdata, minLength);
|
||||
FF_DEBUG("Fastfetch was built without libelf support");
|
||||
return "Fastfetch was built without libelf support";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "common/binary.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/io.h"
|
||||
#include "common/strutil.h"
|
||||
#include "common/windows/nt.h"
|
||||
@@ -17,22 +18,28 @@
|
||||
const char* ffBinaryExtractStrings(const char* peFile, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) {
|
||||
FF_AUTO_CLOSE_FD HANDLE hFile = CreateFileA(peFile, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (hFile == INVALID_HANDLE_VALUE) {
|
||||
FF_DEBUG("CreateFileA() failed: %s", ffDebugWin32Error(GetLastError()));
|
||||
return "CreateFileA() failed";
|
||||
}
|
||||
|
||||
FF_AUTO_CLOSE_FD HANDLE hSection = nullptr;
|
||||
if (!NT_SUCCESS(NtCreateSection(&hSection, SECTION_MAP_READ, nullptr, nullptr, PAGE_READONLY, SEC_COMMIT, hFile))) {
|
||||
NTSTATUS status = NtCreateSection(&hSection, SECTION_MAP_READ, nullptr, nullptr, PAGE_READONLY, SEC_COMMIT, hFile);
|
||||
if (!NT_SUCCESS(status)) {
|
||||
FF_DEBUG("NtCreateSection() failed: %s", ffDebugNtStatus(status));
|
||||
return "NtCreateSection() failed";
|
||||
}
|
||||
|
||||
PVOID base = nullptr;
|
||||
SIZE_T viewSize = 0;
|
||||
if (!NT_SUCCESS(NtMapViewOfSection(hSection, NtCurrentProcess(), &base, 0, 0, nullptr, &viewSize, ViewUnmap, 0, PAGE_READONLY))) {
|
||||
status = NtMapViewOfSection(hSection, NtCurrentProcess(), &base, 0, 0, nullptr, &viewSize, ViewUnmap, 0, PAGE_READONLY);
|
||||
if (!NT_SUCCESS(status)) {
|
||||
FF_DEBUG("NtMapViewOfSection() failed: %s", ffDebugNtStatus(status));
|
||||
return "NtMapViewOfSection() failed";
|
||||
}
|
||||
|
||||
PIMAGE_NT_HEADERS ntHeaders = RtlImageNtHeader(base);
|
||||
if (!ntHeaders) {
|
||||
FF_DEBUG("RtlImageNtHeader() failed");
|
||||
NtUnmapViewOfSection(NtCurrentProcess(), base);
|
||||
return "RtlImageNtHeader() failed";
|
||||
}
|
||||
|
||||
@@ -85,11 +85,29 @@ void ffPrepareCommandOption(FFdata* data) {
|
||||
#define FF_IF_MODULE_MATCH(moduleNameConstant) if (ffStrEqualsIgnCase(moduleType, moduleNameConstant) && !ffStrbufSeparatedContainIgnCaseS(&data->structureDisabled, moduleNameConstant, ':'))
|
||||
|
||||
switch (moduleType[0]) {
|
||||
#if !FF_MODULE_DISABLE_CPUUSAGE
|
||||
#if !FF_MODULE_DISABLE_CPUUSAGE || !FF_MODULE_DISABLE_COMMAND
|
||||
case 'C':
|
||||
case 'c':
|
||||
#endif
|
||||
#if !FF_MODULE_DISABLE_CPUUSAGE
|
||||
FF_IF_MODULE_MATCH(ffCPUUsageModuleInfo.name)
|
||||
ffPrepareCPUUsage(); // The rate is derived from the CPU's own counters; no options are involved
|
||||
#endif
|
||||
|
||||
#if !FF_MODULE_DISABLE_COMMAND
|
||||
FF_IF_MODULE_MATCH(ffCommandModuleInfo.name) {
|
||||
// `command` runs its shell commands in parallel: `ffPrepareCommand` spawns them here,
|
||||
// in the prepare pass, and `ffDetectCommand` collects them in the print pass. The
|
||||
// command line itself can only be read from the JSON config, and this code path does
|
||||
// not consult it, so nothing would ever be queued and the module would report an
|
||||
// internal error marker. Refuse it up front instead of letting it get that far.
|
||||
fputs("Error: module `command` is not supported with `--structure`\n", stderr);
|
||||
fputs(" Its options can only be read from the JSON config, which `--structure` bypasses.\n", stderr);
|
||||
fputs(" Add it to the config instead, e.g. `{ \"modules\": [ { \"type\": \"command\", \"text\": \"uname -r\" } ] }`\n", stderr);
|
||||
exit(481);
|
||||
}
|
||||
#endif
|
||||
#if !FF_MODULE_DISABLE_CPUUSAGE || !FF_MODULE_DISABLE_COMMAND
|
||||
break;
|
||||
#endif
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#ifdef FF_HAVE_DBUS
|
||||
|
||||
#include "common/debug.h"
|
||||
#include "common/thread.h"
|
||||
#include "common/strutil.h"
|
||||
|
||||
@@ -39,11 +40,13 @@ static const FFDBusLibrary* loadLib(void) {
|
||||
const char* ffDBusLoadData(DBusBusType busType, FFDBusData* data) {
|
||||
data->lib = loadLib();
|
||||
if (data->lib == nullptr) {
|
||||
FF_DEBUG("libdbus-1 could not be loaded");
|
||||
return "Failed to load DBus library";
|
||||
}
|
||||
|
||||
data->connection = data->lib->ffdbus_bus_get(busType, nullptr);
|
||||
if (data->connection == nullptr) {
|
||||
FF_DEBUG("dbus_bus_get(busType=%d) returned no connection", (int) busType);
|
||||
return "Failed to connect to DBus";
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
#include <errno.h>
|
||||
|
||||
#ifndef NDEBUG
|
||||
|
||||
#include "common/debug.h"
|
||||
#include "common/thread.h"
|
||||
|
||||
[[gnu::nonnull(1), gnu::pure, gnu::always_inline, nodiscard]]
|
||||
static inline const char* ffFindFileName(const char* file) {
|
||||
const char* lastSlash = __builtin_strrchr(file, '/');
|
||||
#ifdef _WIN32
|
||||
if (lastSlash == nullptr) {
|
||||
lastSlash = __builtin_strrchr(file, '\\');
|
||||
}
|
||||
#endif
|
||||
if (lastSlash != nullptr) {
|
||||
return lastSlash + 1;
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
#if __GNUC__ && !__clang__
|
||||
[[gnu::optimize("O2")]]
|
||||
#endif
|
||||
void ffDebugPrint(const char* file, int line, const char* format, ...) {
|
||||
if (!instance.config.display.debugMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
static FFThreadMutex debugMutex = FF_THREAD_MUTEX_INITIALIZER;
|
||||
// Saved before the lock and restored after the unlock: FF_DEBUG() is called from error paths
|
||||
// that go on to read `errno`, so nothing this function does -- neither the lock nor the
|
||||
// formatting -- may be allowed to leave its own value behind.
|
||||
int errno_ = errno;
|
||||
ffThreadMutexLock(&debugMutex);
|
||||
static char errmsg_[1024]; // Don't increase stack size
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
vsnprintf(errmsg_, sizeof(errmsg_), format, args);
|
||||
va_end(args);
|
||||
fprintf(stderr, "[%s%4d, %s] %s\n", ffFindFileName(file), line, ffTimeToTimeStr(ffTimeGetNow()), errmsg_);
|
||||
ffThreadMutexUnlock(&debugMutex);
|
||||
errno = errno_;
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
#include "common/windows/nt.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include <cfgmgr32.h>
|
||||
|
||||
const char* ffDebugWin32Error(DWORD errorCode) {
|
||||
static char buffer[512];
|
||||
|
||||
wchar_t bufferW[256];
|
||||
ULONG len = FormatMessageW(
|
||||
FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
nullptr,
|
||||
(DWORD) errorCode,
|
||||
0,
|
||||
bufferW,
|
||||
ARRAY_SIZE(bufferW),
|
||||
nullptr);
|
||||
|
||||
if (len == 0) {
|
||||
snprintf(buffer, sizeof(buffer), "Unknown error code (%lu)", errorCode);
|
||||
} else {
|
||||
// Remove trailing newline
|
||||
while (len > 0 && (bufferW[len - 1] == '\r' || bufferW[len - 1] == '\n')) {
|
||||
--len;
|
||||
}
|
||||
|
||||
if (NT_SUCCESS(RtlUnicodeToUTF8N(buffer, sizeof(buffer), &len, bufferW, len * sizeof(wchar_t)))) {
|
||||
snprintf(buffer + len, sizeof(buffer) - len, " (%lu)", errorCode);
|
||||
} else {
|
||||
snprintf(buffer, sizeof(buffer), "Unknown error (%lu)", errorCode);
|
||||
}
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
const char* ffDebugConfigRet(CONFIGRET ret) {
|
||||
return ffDebugWin32Error(CM_MapCrToWin32Err(ret, ERROR_INTERNAL_ERROR));
|
||||
}
|
||||
|
||||
const char* ffDebugNtStatus(NTSTATUS status) {
|
||||
return ffDebugWin32Error(RtlNtStatusToDosError(status));
|
||||
}
|
||||
|
||||
static inline DWORD HRESULTToWin32Error(HRESULT hr) {
|
||||
if (SUCCEEDED(hr)) {
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
if (HRESULT_FACILITY(hr) == FACILITY_WIN32) {
|
||||
return HRESULT_CODE(hr);
|
||||
}
|
||||
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
const char* ffDebugHResult(HRESULT hr) {
|
||||
return ffDebugWin32Error(HRESULTToWin32Error(hr));
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -1,60 +0,0 @@
|
||||
#include "common/debug.h"
|
||||
#include "common/windows/nt.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include <cfgmgr32.h>
|
||||
|
||||
const char* ffDebugWin32Error(DWORD errorCode) {
|
||||
static char buffer[512];
|
||||
|
||||
wchar_t bufferW[256];
|
||||
ULONG len = FormatMessageW(
|
||||
FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
nullptr,
|
||||
(DWORD) errorCode,
|
||||
0,
|
||||
bufferW,
|
||||
ARRAY_SIZE(bufferW),
|
||||
nullptr);
|
||||
|
||||
if (len == 0) {
|
||||
snprintf(buffer, sizeof(buffer), "Unknown error code (%lu)", errorCode);
|
||||
} else {
|
||||
// Remove trailing newline
|
||||
while (len > 0 && (bufferW[len - 1] == '\r' || bufferW[len - 1] == '\n')) {
|
||||
--len;
|
||||
}
|
||||
|
||||
if (NT_SUCCESS(RtlUnicodeToUTF8N(buffer, sizeof(buffer), &len, bufferW, len * sizeof(wchar_t)))) {
|
||||
snprintf(buffer + len, sizeof(buffer) - len, " (%lu)", errorCode);
|
||||
} else {
|
||||
snprintf(buffer, sizeof(buffer), "Unknown error (%lu)", errorCode);
|
||||
}
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
const char* ffDebugConfigRet(CONFIGRET ret) {
|
||||
return ffDebugWin32Error(CM_MapCrToWin32Err(ret, ERROR_INTERNAL_ERROR));
|
||||
}
|
||||
|
||||
const char* ffDebugNtStatus(NTSTATUS status) {
|
||||
return ffDebugWin32Error(RtlNtStatusToDosError(status));
|
||||
}
|
||||
|
||||
static inline DWORD HRESULTToWin32Error(HRESULT hr) {
|
||||
if (SUCCEEDED(hr)) {
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
if (HRESULT_FACILITY(hr) == FACILITY_WIN32) {
|
||||
return HRESULT_CODE(hr);
|
||||
}
|
||||
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
const char* ffDebugHResult(HRESULT hr) {
|
||||
return ffDebugWin32Error(HRESULTToWin32Error(hr));
|
||||
}
|
||||
+77
-205
@@ -147,11 +147,26 @@ static inline bool formatArgSet(const FFformatarg* arg) {
|
||||
static void appendLuaError(FFstrbuf* buffer, const char* prefix, lua_State* L) {
|
||||
const char* err = lua_tolstring(L, -1, nullptr);
|
||||
if (err) {
|
||||
const char* tmp = strchr(err, ':');
|
||||
if (tmp) {
|
||||
err = tmp + 1;
|
||||
while (*err == ' ') {
|
||||
++err;
|
||||
// Drop the position Lua puts in front of an error and keep the message only. Both places
|
||||
// that build one -- `luaG_addinfo` and `luaL_where` -- format it as `%s:%d: %s`, so a
|
||||
// position is a colon, a line number, a colon and a space:
|
||||
// general.preload.lua:1: <name> expected near '=' -> <name> expected near '='
|
||||
// [string ""]:1: x -> x
|
||||
// Not every error has one, and cutting at the first colon mangled those. `luaL_loadfilex`
|
||||
// reports `cannot open <path>: <strerror>`, where that colon is the one after the path, so
|
||||
// the path -- the only part naming the problem -- was dropped and `No such file or
|
||||
// directory` was left on its own. A colon followed by a line number does not identify a
|
||||
// position either, because a path can contain one: `cannot open /tmp/x:12:y/nope.lua: ...`
|
||||
// has to survive whole. The trailing space is what separates the two, and a real position
|
||||
// always has it.
|
||||
for (const char* colon = strchr(err, ':'); colon; colon = strchr(colon + 1, ':')) {
|
||||
const char* line = colon + 1;
|
||||
while (*line >= '0' && *line <= '9') {
|
||||
++line;
|
||||
}
|
||||
if (line > colon + 1 && line[0] == ':' && line[1] == ' ') {
|
||||
err = line + 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -205,7 +220,11 @@ static bool parseLuaString(FFstrbuf* buffer, const char* script, uint32_t script
|
||||
lua_pushboolean(L, *(bool*) arg->value);
|
||||
break;
|
||||
case FF_ARG_TYPE_STRING:
|
||||
lua_pushlstring(L, (const char*) arg->value, strlen((const char*) arg->value));
|
||||
if (!arg->value) {
|
||||
lua_pushlstring(L, "", 0);
|
||||
} else {
|
||||
lua_pushlstring(L, (const char*) arg->value, strlen((const char*) arg->value));
|
||||
}
|
||||
break;
|
||||
case FF_ARG_TYPE_STRBUF: {
|
||||
const FFstrbuf* sb = (const FFstrbuf*) arg->value;
|
||||
@@ -261,208 +280,67 @@ static bool parseLuaString(FFstrbuf* buffer, const char* script, uint32_t script
|
||||
}
|
||||
#endif
|
||||
|
||||
#if FF_HAVE_QUICKJS
|
||||
#include <quickjs.h>
|
||||
|
||||
struct FFQuickJSData {
|
||||
FF_LIBRARY_SYMBOL(JS_NewRuntime)
|
||||
FF_LIBRARY_SYMBOL(JS_NewContext)
|
||||
FF_LIBRARY_SYMBOL(JS_FreeRuntime)
|
||||
FF_LIBRARY_SYMBOL(JS_EvalThis)
|
||||
FF_LIBRARY_SYMBOL(JS_GetException)
|
||||
FF_LIBRARY_SYMBOL(JS_ToCStringLen2)
|
||||
FF_LIBRARY_SYMBOL(JS_FreeCString)
|
||||
FF_LIBRARY_SYMBOL(JS_NewStringLen)
|
||||
FF_LIBRARY_SYMBOL(JS_NewArray)
|
||||
FF_LIBRARY_SYMBOL(JS_NewBigUint64)
|
||||
FF_LIBRARY_SYMBOL(JS_SetPropertyUint32)
|
||||
FF_LIBRARY_SYMBOL(JS_NewObject)
|
||||
FF_LIBRARY_SYMBOL(JS_SetPropertyStr)
|
||||
FF_LIBRARY_SYMBOL(JS_FreeValue)
|
||||
|
||||
JSRuntime* rt;
|
||||
JSContext* ctx;
|
||||
bool inited;
|
||||
} qjsData;
|
||||
|
||||
static const char* loadQuickJSState(void) {
|
||||
if (qjsData.inited) {
|
||||
if (qjsData.ctx == nullptr) {
|
||||
return "QuickJS is not available";
|
||||
}
|
||||
return nullptr;
|
||||
// Returns the length of the ANSI CSI sequence at `str`, or 0 if there is none.
|
||||
// A CSI sequence is `ESC [` + parameter bytes (0x30-0x3F) + intermediate bytes (0x20-0x2F) + final byte (0x40-0x7E)
|
||||
static uint32_t ansiEscapeLength(const char* str) {
|
||||
if (str[0] != '\e' || str[1] != '[') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
qjsData.inited = true;
|
||||
#ifdef _WIN32
|
||||
FF_LIBRARY_LOAD_MESSAGE(libqjs, "libqjs-0" FF_LIBRARY_EXTENSION, 0)
|
||||
#else
|
||||
FF_LIBRARY_LOAD_MESSAGE(libqjs, "libqjs" FF_LIBRARY_EXTENSION, 0)
|
||||
#endif
|
||||
|
||||
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_NewRuntime)
|
||||
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_NewContext)
|
||||
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_FreeRuntime)
|
||||
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_EvalThis)
|
||||
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_GetException)
|
||||
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_ToCStringLen2)
|
||||
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_FreeCString)
|
||||
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_NewStringLen)
|
||||
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_NewArray)
|
||||
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_SetPropertyUint32)
|
||||
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_NewObject)
|
||||
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_SetPropertyStr)
|
||||
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_FreeValue)
|
||||
|
||||
qjsData.rt = qjsData.ffJS_NewRuntime();
|
||||
if (qjsData.rt == nullptr) {
|
||||
return "JS_NewRuntime() failed";
|
||||
const char* p = str + 2;
|
||||
while (*p >= '0' && *p <= '?') {
|
||||
++p;
|
||||
}
|
||||
|
||||
qjsData.ctx = qjsData.ffJS_NewContext(qjsData.rt);
|
||||
if (qjsData.ctx == nullptr) {
|
||||
qjsData.ffJS_FreeRuntime(qjsData.rt);
|
||||
qjsData.rt = nullptr;
|
||||
return "JS_NewContext() failed";
|
||||
while (*p >= ' ' && *p <= '/') {
|
||||
++p;
|
||||
}
|
||||
|
||||
libqjs = nullptr; // don't close quickjs
|
||||
|
||||
return nullptr;
|
||||
if (*p < '@' || *p > '~') {
|
||||
return 0; // not a complete CSI sequence
|
||||
}
|
||||
return (uint32_t) (p - str) + 1;
|
||||
}
|
||||
|
||||
static bool parseQuickJSString(FFstrbuf* buffer, const char* script, uint32_t scriptLen, uint32_t numArgs, const FFformatarg* arguments) {
|
||||
const char* err = loadQuickJSState();
|
||||
if (err) {
|
||||
ffStrbufAppendF(buffer, "Qjs init error: %s", err);
|
||||
return false;
|
||||
}
|
||||
JSContext* ctx = qjsData.ctx;
|
||||
JSValue argsObj = qjsData.ffJS_NewObject(ctx);
|
||||
FF_STRBUF_AUTO_DESTROY argNameBuf = ffStrbufCreate();
|
||||
|
||||
for (uint32_t i = 0; i < numArgs; ++i) {
|
||||
const FFformatarg* arg = &arguments[i];
|
||||
|
||||
JSValue value;
|
||||
switch (arg->type) {
|
||||
case FF_ARG_TYPE_INT:
|
||||
value = JS_NewInt32(ctx, *(int32_t*) arg->value);
|
||||
break;
|
||||
case FF_ARG_TYPE_UINT:
|
||||
value = JS_NewUint32(ctx, *(uint32_t*) arg->value);
|
||||
break;
|
||||
case FF_ARG_TYPE_UINT64: {
|
||||
uint64_t val = *(uint64_t*) arg->value;
|
||||
if (val <= INT32_MAX) {
|
||||
value = JS_NewInt32(ctx, (int32_t) val);
|
||||
} else {
|
||||
value = JS_NewFloat64(ctx, (double) val);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case FF_ARG_TYPE_UINT16:
|
||||
value = JS_NewUint32(ctx, *(uint16_t*) arg->value);
|
||||
break;
|
||||
case FF_ARG_TYPE_UINT8:
|
||||
value = JS_NewUint32(ctx, *(uint8_t*) arg->value);
|
||||
break;
|
||||
case FF_ARG_TYPE_FLOAT:
|
||||
value = JS_NewFloat64(ctx, *(float*) arg->value);
|
||||
break;
|
||||
case FF_ARG_TYPE_DOUBLE:
|
||||
value = JS_NewFloat64(ctx, *(double*) arg->value);
|
||||
break;
|
||||
case FF_ARG_TYPE_BOOL:
|
||||
value = JS_NewBool(ctx, *(bool*) arg->value);
|
||||
break;
|
||||
case FF_ARG_TYPE_STRING:
|
||||
value = qjsData.ffJS_NewStringLen(ctx, (const char*) arg->value, strlen((const char*) arg->value));
|
||||
break;
|
||||
case FF_ARG_TYPE_STRBUF: {
|
||||
const FFstrbuf* sb = (const FFstrbuf*) arg->value;
|
||||
value = qjsData.ffJS_NewStringLen(ctx, sb->chars, sb->length);
|
||||
break;
|
||||
}
|
||||
case FF_ARG_TYPE_LIST: {
|
||||
const FFlist* list = (const FFlist*) arg->value;
|
||||
JSValue arr = qjsData.ffJS_NewArray(ctx);
|
||||
for (uint32_t li = 0; li < list->length; ++li) {
|
||||
const FFstrbuf* item = FF_LIST_GET(FFstrbuf, *list, li);
|
||||
JSValue itemValue = qjsData.ffJS_NewStringLen(ctx, item->chars, item->length);
|
||||
qjsData.ffJS_SetPropertyUint32(ctx, arr, li, itemValue);
|
||||
}
|
||||
|
||||
value = arr;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
value = JS_UNDEFINED;
|
||||
break;
|
||||
}
|
||||
|
||||
if (arg->name && arg->name[0]) {
|
||||
normalizeArgName(&argNameBuf, arg->name);
|
||||
} else {
|
||||
ffStrbufSetF(&argNameBuf, "arg%" PRIu32, i + 1);
|
||||
}
|
||||
qjsData.ffJS_SetPropertyStr(ctx, argsObj, argNameBuf.chars, value);
|
||||
}
|
||||
JSValue result = qjsData.ffJS_EvalThis(ctx, argsObj, script, scriptLen, "", JS_EVAL_TYPE_GLOBAL | JS_EVAL_FLAG_STRICT);
|
||||
|
||||
qjsData.ffJS_FreeValue(ctx, argsObj);
|
||||
|
||||
bool ret = false;
|
||||
if (JS_IsException(result)) {
|
||||
JSValue exc = qjsData.ffJS_GetException(ctx);
|
||||
const char* message = qjsData.ffJS_ToCStringLen2(ctx, nullptr, exc, false);
|
||||
qjsData.ffJS_FreeValue(ctx, exc);
|
||||
ffStrbufAppendF(buffer, "Qjs runtime error: %s", message ?: "unknown");
|
||||
if (message) {
|
||||
qjsData.ffJS_FreeCString(ctx, message);
|
||||
}
|
||||
} else if (JS_IsUndefined(result)) {
|
||||
ffStrbufAppendS(buffer, "Qjs result error: undefined result");
|
||||
} else {
|
||||
size_t len;
|
||||
const char* res = qjsData.ffJS_ToCStringLen2(ctx, &len, result, false);
|
||||
if (res) {
|
||||
ffStrbufAppendNS(buffer, (uint32_t) len, res);
|
||||
qjsData.ffJS_FreeCString(ctx, res);
|
||||
}
|
||||
ret = true;
|
||||
}
|
||||
|
||||
qjsData.ffJS_FreeValue(ctx, result);
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
|
||||
static bool skipAnsiEscape(FFstrbuf* in, FFstrbuf* out, FFstrbuf* trailingEscape) {
|
||||
if (__builtin_expect(in->chars[0] == '\e' && in->chars[1] == '[', false)) {
|
||||
// skip ANSI escape codes at the start of the string for truncation
|
||||
const char* p = in->chars + 2;
|
||||
while (*p && (ffCharIsDigit(*p) || *p == ';')) {
|
||||
++p;
|
||||
// Skip all ANSI escape codes at the start of the string, so that they don't count
|
||||
// towards the truncation / padding length. A value may carry more than one of them,
|
||||
// e.g. `\e[31m\e[1m`.
|
||||
uint32_t prefixLen = 0;
|
||||
while (in->length - prefixLen >= 2) {
|
||||
uint32_t len = ansiEscapeLength(in->chars + prefixLen);
|
||||
if (len == 0) {
|
||||
break;
|
||||
}
|
||||
if (*p && isascii(*p)) {
|
||||
++p;
|
||||
}
|
||||
uint32_t prefixLen = (uint32_t) (p - in->chars);
|
||||
ffStrbufAppendNS(out, prefixLen, in->chars);
|
||||
ffStrbufSubstrAfter(in, prefixLen - 1);
|
||||
|
||||
if (trailingEscape) {
|
||||
// likely have a `CSI m` reset at the end of the string
|
||||
uint32_t iLastEscape = ffStrbufLastIndexC(in, '\e');
|
||||
if (iLastEscape != in->length) {
|
||||
ffStrbufSetNS(trailingEscape, in->length - iLastEscape, in->chars + iLastEscape);
|
||||
ffStrbufSubstrBefore(in, iLastEscape);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
prefixLen += len;
|
||||
}
|
||||
return false;
|
||||
|
||||
if (prefixLen == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ffStrbufAppendNS(out, prefixLen, in->chars);
|
||||
ffStrbufSubstrAfter(in, prefixLen - 1);
|
||||
|
||||
if (trailingEscape) {
|
||||
// likely have one or more `CSI m` resets at the end of the string
|
||||
uint32_t end = in->length;
|
||||
while (end >= 3) {
|
||||
uint32_t start = end - 1;
|
||||
while (start > 0 && in->chars[start] != '\e') {
|
||||
--start;
|
||||
}
|
||||
uint32_t len = ansiEscapeLength(in->chars + start);
|
||||
if (len == 0 || start + len != end) {
|
||||
break;
|
||||
}
|
||||
end = start;
|
||||
}
|
||||
|
||||
if (end < in->length) {
|
||||
ffStrbufSetNS(trailingEscape, in->length - end, in->chars + end);
|
||||
ffStrbufSubstrBefore(in, end);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool parseFormatString(FFstrbuf* buffer, const FFstrbuf* formatstr, uint32_t numArgs, const FFformatarg* arguments) {
|
||||
@@ -758,12 +636,6 @@ static bool parseFormatString(FFstrbuf* buffer, const FFstrbuf* formatstr, uint3
|
||||
}
|
||||
|
||||
bool ffParseFormatString(FFstrbuf* buffer, const FFstrbuf* formatstr, uint32_t numArgs, const FFformatarg* arguments) {
|
||||
#if FF_HAVE_QUICKJS
|
||||
if (ffStrbufStartsWithS(formatstr, "qjs:")) {
|
||||
return parseQuickJSString(buffer, formatstr->chars + 4, formatstr->length - 4, numArgs, arguments);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if FF_HAVE_LUA
|
||||
if (ffStrbufStartsWithS(formatstr, "lua:")) {
|
||||
// If outputFormat starts with "lua:", treat the rest as a Lua script
|
||||
|
||||
+97
-18
@@ -26,15 +26,16 @@
|
||||
// 0: title
|
||||
// 1: blank
|
||||
// 2: logo type row
|
||||
// 3: output mode row
|
||||
// 4: blank
|
||||
// 5: modules title row
|
||||
// 6: blank
|
||||
// 7..7+listRows-1: module grid
|
||||
// 7+listRows: description row
|
||||
// 8+listRows: help line 1
|
||||
// 9+listRows: help line 2
|
||||
#define FF_GEN_CONFIG_LIST_TOP 7
|
||||
// 3: logo position row
|
||||
// 4: output mode row
|
||||
// 5: blank
|
||||
// 6: modules title row
|
||||
// 7: blank
|
||||
// 8..8+listRows-1: module grid
|
||||
// 8+listRows: description row
|
||||
// 9+listRows: help line 1
|
||||
// 10+listRows: help line 2
|
||||
#define FF_GEN_CONFIG_LIST_TOP 8
|
||||
#define FF_GEN_CONFIG_BOTTOM_CHROME 3
|
||||
|
||||
// Poll wait timeout (ms) while idle. A longer wait avoids frequent full-screen
|
||||
@@ -66,6 +67,7 @@ typedef struct FFGenConfigUI {
|
||||
uint32_t cursor;
|
||||
uint32_t viewOffset;
|
||||
FFLogoType logoType;
|
||||
FFLogoPosition logoPosition;
|
||||
bool fullConfig;
|
||||
bool confirmingOverwrite;
|
||||
uint16_t rows;
|
||||
@@ -165,7 +167,7 @@ static void enterRawMode(void) {
|
||||
|
||||
static void getTerminalSize(uint16_t* rows, uint16_t* cols) {
|
||||
FFTerminalSizeResult size;
|
||||
if (ffDetectTerminalSize(&size) && size.rows > 0 && size.columns > 0) {
|
||||
if (ffDetectTerminalSize(&size, true) && size.rows > 0 && size.columns > 0) {
|
||||
*rows = size.rows;
|
||||
*cols = size.columns;
|
||||
} else {
|
||||
@@ -248,8 +250,8 @@ static void removeAllBreaksSeparators(FFGenConfigUI* ui) {
|
||||
|
||||
static void computeLayout(FFGenConfigUI* ui) {
|
||||
FFGenConfigLayout* layout = &ui->layout;
|
||||
if (ui->rows < 10) {
|
||||
ui->rows = 10;
|
||||
if (ui->rows < 11) {
|
||||
ui->rows = 11;
|
||||
}
|
||||
int32_t listRows = (int32_t) ui->rows - FF_GEN_CONFIG_LIST_TOP - FF_GEN_CONFIG_BOTTOM_CHROME;
|
||||
layout->listRows = listRows < 0 ? 0 : (uint16_t) listRows;
|
||||
@@ -367,6 +369,26 @@ static void cycleLogoType(FFGenConfigUI* ui, int delta) {
|
||||
}
|
||||
}
|
||||
|
||||
static void cycleLogoPosition(FFGenConfigUI* ui, int delta) {
|
||||
static const FFLogoPosition positions[] = {
|
||||
FF_LOGO_POSITION_AUTO,
|
||||
FF_LOGO_POSITION_LEFT,
|
||||
FF_LOGO_POSITION_RIGHT,
|
||||
FF_LOGO_POSITION_TOP,
|
||||
};
|
||||
|
||||
uint32_t index = 0;
|
||||
for (uint32_t i = 0; i < ARRAY_SIZE(positions); ++i) {
|
||||
if (positions[i] == ui->logoPosition) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
index = (index + (delta > 0 ? 1 : ARRAY_SIZE(positions) - 1)) % ARRAY_SIZE(positions);
|
||||
ui->logoPosition = positions[index];
|
||||
}
|
||||
|
||||
static uint32_t countSelectedModules(const FFlist* items) {
|
||||
uint32_t count = 0;
|
||||
FF_LIST_FOR_EACH (FFGenConfigItem, item, *items) {
|
||||
@@ -558,7 +580,46 @@ static void renderFrame(FFGenConfigUI* ui, FFstrbuf* out) {
|
||||
}
|
||||
finishRow(&row, out, ++rowCount == ui->rows);
|
||||
|
||||
// Row 3: output mode
|
||||
// Row 3: logo position
|
||||
rowInit(&row, cols);
|
||||
rowAppendVisual(&row, " ");
|
||||
rowAppendRaw(&row, "\e[" FF_COLOR_MODE_BOLD "m");
|
||||
rowAppendVisual(&row, "Logo ");
|
||||
rowAppendRaw(&row, "\e[" FF_COLOR_MODE_UNDERLINE_ "m");
|
||||
rowAppendVisual(&row, "p");
|
||||
rowAppendRaw(&row, "\e[24m"); // Reset underline
|
||||
rowAppendVisual(&row, "osition:");
|
||||
rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET);
|
||||
rowAppendVisual(&row, " ");
|
||||
drawLogoOption(&row, "auto", ui->logoPosition == FF_LOGO_POSITION_AUTO);
|
||||
rowAppendVisual(&row, " ");
|
||||
drawLogoOption(&row, "left", ui->logoPosition == FF_LOGO_POSITION_LEFT);
|
||||
rowAppendVisual(&row, " ");
|
||||
drawLogoOption(&row, "right", ui->logoPosition == FF_LOGO_POSITION_RIGHT);
|
||||
rowAppendVisual(&row, " ");
|
||||
drawLogoOption(&row, "top", ui->logoPosition == FF_LOGO_POSITION_TOP);
|
||||
if (cols >= 120) {
|
||||
rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_SOFT);
|
||||
switch (ui->logoPosition) {
|
||||
case FF_LOGO_POSITION_AUTO:
|
||||
rowAppendVisual(&row, " - Choose left or top from terminal width");
|
||||
break;
|
||||
case FF_LOGO_POSITION_RIGHT:
|
||||
rowAppendVisual(&row, " - Place the logo to the right");
|
||||
break;
|
||||
case FF_LOGO_POSITION_TOP:
|
||||
rowAppendVisual(&row, " - Place the logo above module output");
|
||||
break;
|
||||
case FF_LOGO_POSITION_LEFT:
|
||||
default:
|
||||
rowAppendVisual(&row, " - Place the logo to the left");
|
||||
break;
|
||||
}
|
||||
rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET);
|
||||
}
|
||||
finishRow(&row, out, ++rowCount == ui->rows);
|
||||
|
||||
// Row 4: output mode
|
||||
rowInit(&row, cols);
|
||||
rowAppendVisual(&row, " ");
|
||||
rowAppendRaw(&row, "\e[" FF_COLOR_MODE_BOLD FF_COLOR_MODE_UNDERLINE_ "m");
|
||||
@@ -581,11 +642,11 @@ static void renderFrame(FFGenConfigUI* ui, FFstrbuf* out) {
|
||||
}
|
||||
finishRow(&row, out, ++rowCount == ui->rows);
|
||||
|
||||
// Row 4: blank
|
||||
// Row 5: blank
|
||||
rowInit(&row, cols);
|
||||
finishRow(&row, out, ++rowCount == ui->rows);
|
||||
|
||||
// Row 5: modules title
|
||||
// Row 6: modules title
|
||||
rowInit(&row, cols);
|
||||
rowAppendVisual(&row, " ");
|
||||
rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_BOLT);
|
||||
@@ -596,7 +657,7 @@ static void renderFrame(FFGenConfigUI* ui, FFstrbuf* out) {
|
||||
rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET);
|
||||
finishRow(&row, out, ++rowCount == ui->rows);
|
||||
|
||||
// Row 6: blank
|
||||
// Row 7: blank
|
||||
rowInit(&row, cols);
|
||||
finishRow(&row, out, ++rowCount == ui->rows);
|
||||
|
||||
@@ -618,7 +679,7 @@ static void renderFrame(FFGenConfigUI* ui, FFstrbuf* out) {
|
||||
finishRow(&row, out, ++rowCount == ui->rows);
|
||||
}
|
||||
|
||||
// Row after grid: description
|
||||
// Row 9+listRows: description
|
||||
rowInit(&row, cols);
|
||||
if (ui->cursor < ui->items.length) {
|
||||
const FFGenConfigItem* item = FF_LIST_GET(FFGenConfigItem, ui->items, ui->cursor);
|
||||
@@ -650,7 +711,7 @@ static void renderFrame(FFGenConfigUI* ui, FFstrbuf* out) {
|
||||
|
||||
rowInit(&row, cols);
|
||||
rowAppendRaw(&row, "\e[" FF_COLOR_MODE_DIM_ "m");
|
||||
rowAppendVisual(&row, " l/L logo o minimal/full s/Enter save q/Esc quit g/G top/bottom");
|
||||
rowAppendVisual(&row, " l/L logo p/P position o minimal/full s/Enter save q/Esc quit g/G top/bottom");
|
||||
rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET);
|
||||
finishRow(&row, out, ++rowCount == ui->rows);
|
||||
|
||||
@@ -904,6 +965,10 @@ static int handleKey(FFGenConfigUI* ui, FFGenConfigKey key, char ch, bool fileEx
|
||||
cycleLogoType(ui, 1);
|
||||
} else if (ch == 'L') {
|
||||
cycleLogoType(ui, -1);
|
||||
} else if (ch == 'p') {
|
||||
cycleLogoPosition(ui, 1);
|
||||
} else if (ch == 'P') {
|
||||
cycleLogoPosition(ui, -1);
|
||||
} else if (ch == '\x03' || ch == '\x04' || ch == '\x1a' || ch == '\x1c') {
|
||||
return 0;
|
||||
}
|
||||
@@ -940,6 +1005,18 @@ static FFLogoType initialLogoType(void) {
|
||||
}
|
||||
}
|
||||
|
||||
static FFLogoPosition initialLogoPosition(void) {
|
||||
switch (instance.config.logo.position) {
|
||||
case FF_LOGO_POSITION_TOP:
|
||||
case FF_LOGO_POSITION_RIGHT:
|
||||
case FF_LOGO_POSITION_AUTO:
|
||||
return instance.config.logo.position;
|
||||
case FF_LOGO_POSITION_LEFT:
|
||||
default:
|
||||
return FF_LOGO_POSITION_LEFT;
|
||||
}
|
||||
}
|
||||
|
||||
static bool applyConfig(FFdata* data, const FFlist* items, bool fullConfig) {
|
||||
if (data->resultDoc) {
|
||||
fputs("Error: duplicated `--gen-config` or `--format json` flags found\n", stderr);
|
||||
@@ -972,6 +1049,7 @@ bool ffGenConfigInteractive(FFdata* data) {
|
||||
.cursor = 0,
|
||||
.viewOffset = 0,
|
||||
.logoType = initialLogoType(),
|
||||
.logoPosition = initialLogoPosition(),
|
||||
.fullConfig = false,
|
||||
.confirmingOverwrite = false,
|
||||
.rows = 24,
|
||||
@@ -993,6 +1071,7 @@ bool ffGenConfigInteractive(FFdata* data) {
|
||||
if (result == 1) {
|
||||
if (applyConfig(data, &ui.items, ui.fullConfig)) {
|
||||
instance.config.logo.type = ui.logoType;
|
||||
instance.config.logo.position = ui.logoPosition;
|
||||
success = true;
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -178,9 +178,6 @@ void ffDestroyInstance(void) {
|
||||
#if FF_HAVE_LUA
|
||||
#include <lua.h>
|
||||
#endif
|
||||
#if FF_HAVE_QUICKJS
|
||||
#include <quickjs.h>
|
||||
#endif
|
||||
|
||||
// Must be in a file compiled with the libfastfetch target, because the FF_HAVE* macros are not defined for the executable targets
|
||||
void ffListFeatures(void) {
|
||||
@@ -248,12 +245,6 @@ void ffListFeatures(void) {
|
||||
#if FF_HAVE_FREETYPE
|
||||
"freetype\n"
|
||||
#endif
|
||||
#if FF_HAVE_PULSE
|
||||
"libpulse\n"
|
||||
#endif
|
||||
#if FF_HAVE_DDCUTIL
|
||||
"libddcutil\n"
|
||||
#endif
|
||||
#if FF_HAVE_ELF || __sun || (__FreeBSD__ && !__DragonFly__) || __OpenBSD__ || __NetBSD__
|
||||
"libelf\n"
|
||||
#endif
|
||||
@@ -295,9 +286,6 @@ void ffListFeatures(void) {
|
||||
#endif
|
||||
#if FF_HAVE_LUA
|
||||
LUA_VERSION "\n"
|
||||
#endif
|
||||
#if FF_HAVE_QUICKJS
|
||||
"QuickJS " FF_STR(QJS_VERSION_MAJOR) "." FF_STR(QJS_VERSION_MINOR) "." FF_STR(QJS_VERSION_PATCH) QJS_VERSION_SUFFIX "\n"
|
||||
#endif
|
||||
"",
|
||||
stdout);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "common/io.h"
|
||||
#include "fastfetch.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/strutil.h"
|
||||
#include "common/time.h"
|
||||
|
||||
@@ -16,7 +17,7 @@
|
||||
|
||||
#if FF_HAVE_WORDEXP
|
||||
#include <wordexp.h>
|
||||
#else
|
||||
#elif FF_HAVE_GLOB
|
||||
#include <glob.h>
|
||||
#endif
|
||||
|
||||
@@ -41,9 +42,11 @@ bool ffWriteFileData(const char* fileName, size_t dataSize, const void* data) {
|
||||
createSubfolders(fileName);
|
||||
fd = open(fileName, openFlagsModes, openFlagsRights);
|
||||
if (fd == -1) {
|
||||
FF_DEBUG("open(%s) failed after creating the parent folders: %s", fileName, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
FF_DEBUG("open(%s) failed: %s", fileName, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -109,7 +112,7 @@ bool ffPathExpandEnv(const char* in, FFstrbuf* out) {
|
||||
|
||||
wordfree(&exp);
|
||||
|
||||
#else
|
||||
#elif FF_HAVE_GLOB
|
||||
|
||||
glob_t gb;
|
||||
if (glob(in, GLOB_NOSORT
|
||||
@@ -131,6 +134,10 @@ bool ffPathExpandEnv(const char* in, FFstrbuf* out) {
|
||||
|
||||
globfree(&gb);
|
||||
|
||||
#else
|
||||
|
||||
FF_UNUSED(in, out)
|
||||
|
||||
#endif
|
||||
|
||||
return result;
|
||||
@@ -146,16 +153,19 @@ const char* ffGetTerminalResponse(const char* request, int nParams, const char*
|
||||
if (ftty < 0) {
|
||||
ftty = open("/dev/tty", O_RDWR | O_NOCTTY | O_CLOEXEC);
|
||||
if (ftty < 0) {
|
||||
FF_DEBUG("open(/dev/tty) failed: %s", strerror(errno));
|
||||
return "open(\"/dev/tty\", O_RDWR | O_NOCTTY | O_CLOEXEC) failed";
|
||||
}
|
||||
|
||||
if (tcgetattr(ftty, &oldTerm) == -1) {
|
||||
FF_DEBUG("tcgetattr(/dev/tty) failed: %s", strerror(errno));
|
||||
return "tcgetattr(STDIN_FILENO, &oldTerm) failed";
|
||||
}
|
||||
|
||||
struct termios newTerm = oldTerm;
|
||||
newTerm.c_lflag &= (tcflag_t) ~(ICANON | ECHO);
|
||||
if (tcsetattr(ftty, TCSAFLUSH, &newTerm) == -1) {
|
||||
FF_DEBUG("tcsetattr(/dev/tty) failed: %s", strerror(errno));
|
||||
return "tcsetattr(STDIN_FILENO, TCSAFLUSH, &newTerm)";
|
||||
}
|
||||
atexit(restoreTerm);
|
||||
@@ -165,7 +175,10 @@ const char* ffGetTerminalResponse(const char* request, int nParams, const char*
|
||||
|
||||
// Give the terminal some time to respond
|
||||
#ifndef __APPLE__
|
||||
if (poll(&(struct pollfd) { .fd = ftty, .events = POLLIN }, 1, FF_IO_TERM_RESP_WAIT_MS) <= 0) {
|
||||
const int pollResult = poll(&(struct pollfd) { .fd = ftty, .events = POLLIN }, 1, FF_IO_TERM_RESP_WAIT_MS);
|
||||
if (pollResult <= 0) {
|
||||
FF_DEBUG("poll(/dev/tty) returned %d after %d ms: %s",
|
||||
pollResult, FF_IO_TERM_RESP_WAIT_MS, pollResult == 0 ? "timed out" : strerror(errno));
|
||||
return "poll(/dev/tty) timeout or failed";
|
||||
}
|
||||
#else
|
||||
@@ -175,7 +188,10 @@ const char* ffGetTerminalResponse(const char* request, int nParams, const char*
|
||||
fd_set rd;
|
||||
FD_ZERO(&rd);
|
||||
FD_SET(ftty, &rd);
|
||||
if (select(ftty + 1, &rd, nullptr, nullptr, &(struct timeval) { .tv_sec = FF_IO_TERM_RESP_WAIT_MS / 1000, .tv_usec = (FF_IO_TERM_RESP_WAIT_MS % 1000) * 1000 }) <= 0) {
|
||||
const int selectResult = select(ftty + 1, &rd, nullptr, nullptr, &(struct timeval) { .tv_sec = FF_IO_TERM_RESP_WAIT_MS / 1000, .tv_usec = (FF_IO_TERM_RESP_WAIT_MS % 1000) * 1000 });
|
||||
if (selectResult <= 0) {
|
||||
FF_DEBUG("select(/dev/tty) returned %d after %d ms: %s",
|
||||
selectResult, FF_IO_TERM_RESP_WAIT_MS, selectResult == 0 ? "timed out" : strerror(errno));
|
||||
return "select(/dev/tty) timeout or failed";
|
||||
}
|
||||
}
|
||||
@@ -191,6 +207,8 @@ const char* ffGetTerminalResponse(const char* request, int nParams, const char*
|
||||
ssize_t nRead = read(ftty, buffer + bytesRead, sizeof(buffer) - bytesRead - 1);
|
||||
|
||||
if (nRead <= 0) {
|
||||
FF_DEBUG("read(/dev/tty) returned %zd after %zu bytes: %s",
|
||||
nRead, bytesRead, nRead == 0 ? "the terminal closed the connection" : strerror(errno));
|
||||
va_end(args);
|
||||
return "read(STDIN_FILENO, buffer, sizeof(buffer) - 1) failed";
|
||||
}
|
||||
@@ -204,6 +222,7 @@ const char* ffGetTerminalResponse(const char* request, int nParams, const char*
|
||||
va_end(cargs);
|
||||
|
||||
if (ret <= 0) {
|
||||
FF_DEBUG("vsscanf(\"%s\") matched %d of %d parameters", buffer, ret, nParams);
|
||||
va_end(args);
|
||||
return "vsscanf(buffer, format, args) failed";
|
||||
}
|
||||
@@ -329,6 +348,10 @@ FFNativeFD ffGetNullFD(void) {
|
||||
return hNullFile;
|
||||
}
|
||||
|
||||
bool ffIsTerminal(int fd) {
|
||||
return isatty(fd) != 0;
|
||||
}
|
||||
|
||||
bool ffRemoveFile(const char* fileName) {
|
||||
return unlink(fileName) == 0;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,20 @@
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
// TODO: long paths on Windows.
|
||||
//
|
||||
// Every path that crosses into the wide API from this file is converted into a fixed
|
||||
// `wchar_t[MAX_PATH]` buffer, and `RtlUTF8ToUnicodeN` refuses to fill it for a path of MAX_PATH
|
||||
// characters or more (STATUS_BUFFER_TOO_SMALL) -- which makes each of those call sites a silent
|
||||
// failure above that length rather than an error anyone sees. Raising a buffer on its own does not
|
||||
// help: the wide APIs accept a longer path only either with a `\\?\` prefix on every absolute path
|
||||
// (which also switches off the normalization the rest of the code relies on) or with a
|
||||
// `longPathAware` manifest plus the LongPathsEnabled registry value.
|
||||
//
|
||||
// The plan is the second route: define MAX_PATH as 32768 program-wide, add the manifest flag, and
|
||||
// size these buffers from the UTF-8 input rather than from the constant. It has to be done in one
|
||||
// pass, which is why it is recorded here instead of being applied at the call sites the review
|
||||
// happened to look at.
|
||||
static bool createSubfolders(wchar_t* fileName) {
|
||||
HANDLE hRoot = ffGetPeb()->ProcessParameters->CurrentDirectory.Handle;
|
||||
bool closeRoot = false;
|
||||
@@ -348,8 +362,18 @@ void listFilesRecursively(uint32_t baseLength, FFstrbuf* folder, uint8_t indenta
|
||||
}
|
||||
|
||||
ffStrbufAppendC(folder, '*');
|
||||
WIN32_FIND_DATAA entry;
|
||||
HANDLE hFind = FindFirstFileA(folder->chars, &entry);
|
||||
// The wide API on purpose: every path fastfetch holds is UTF-8, and FindFirstFileA would read
|
||||
// it in the active code page, so a directory whose name the code page cannot represent -- a
|
||||
// Chinese or Japanese name under an English or German locale, for one -- answers
|
||||
// ERROR_PATH_NOT_FOUND and the whole listing disappears. The names it returns are converted
|
||||
// back to UTF-8 below.
|
||||
wchar_t wideFolder[PATH_MAX];
|
||||
if (!NT_SUCCESS(RtlUTF8ToUnicodeN(wideFolder, sizeof(wideFolder), nullptr, folder->chars, folder->length + 1))) {
|
||||
ffStrbufTrimRight(folder, '*');
|
||||
return;
|
||||
}
|
||||
WIN32_FIND_DATAW entry;
|
||||
HANDLE hFind = FindFirstFileW(wideFolder, &entry);
|
||||
ffStrbufTrimRight(folder, '*');
|
||||
if (hFind == INVALID_HANDLE_VALUE) {
|
||||
return;
|
||||
@@ -357,14 +381,15 @@ void listFilesRecursively(uint32_t baseLength, FFstrbuf* folder, uint8_t indenta
|
||||
|
||||
do {
|
||||
if (entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
|
||||
if (ffStrEquals(entry.cFileName, ".") || ffStrEquals(entry.cFileName, "..")) {
|
||||
if (entry.cFileName[0] == L'.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
ffStrbufSubstrBefore(folder, folderLength);
|
||||
ffStrbufAppendS(folder, entry.cFileName);
|
||||
ffStrbufAppendWS(folder, entry.cFileName);
|
||||
ffStrbufAppendC(folder, '/');
|
||||
listFilesRecursively(baseLength, folder, (uint8_t) (indentation + 1), entry.cFileName, pretty);
|
||||
FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateWS(entry.cFileName);
|
||||
listFilesRecursively(baseLength, folder, (uint8_t) (indentation + 1), name.chars, pretty);
|
||||
ffStrbufSubstrBefore(folder, folderLength);
|
||||
continue;
|
||||
}
|
||||
@@ -377,8 +402,9 @@ void listFilesRecursively(uint32_t baseLength, FFstrbuf* folder, uint8_t indenta
|
||||
fputs(folder->chars + baseLength, stdout);
|
||||
}
|
||||
|
||||
puts(entry.cFileName);
|
||||
} while (FindNextFileA(hFind, &entry));
|
||||
FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateWS(entry.cFileName);
|
||||
ffStrbufPutTo(&name, stdout);
|
||||
} while (FindNextFileW(hFind, &entry));
|
||||
FindClose(hFind);
|
||||
}
|
||||
|
||||
@@ -415,7 +441,9 @@ const char* ffGetTerminalResponse(const char* request, int nParams, const char*
|
||||
}
|
||||
|
||||
while (true) {
|
||||
if (NtWaitForSingleObject(hInput, FALSE, &(LARGE_INTEGER) { .QuadPart = (int64_t) FF_IO_TERM_RESP_WAIT_MS * -10000 }) != STATUS_WAIT_0) {
|
||||
NTSTATUS status = NtWaitForSingleObject(hInput, FALSE, &(LARGE_INTEGER) { .QuadPart = (int64_t) FF_IO_TERM_RESP_WAIT_MS * -10000 });
|
||||
if (status != STATUS_WAIT_0) {
|
||||
FF_DEBUG("NtWaitForSingleObject() failed or timeout: %s", ffDebugNtStatus(status));
|
||||
SetConsoleMode(hInput, inputMode);
|
||||
return "NtWaitForSingleObject() failed or timeout";
|
||||
}
|
||||
@@ -445,7 +473,9 @@ const char* ffGetTerminalResponse(const char* request, int nParams, const char*
|
||||
|
||||
while (true) {
|
||||
DWORD bytes = 0;
|
||||
if (!ReadFile(hInput, buffer + bytesRead, (DWORD) (sizeof(buffer) - 1 - bytesRead), &bytes, nullptr) || bytes == 0) {
|
||||
BOOL readOk = ReadFile(hInput, buffer + bytesRead, (DWORD) (sizeof(buffer) - 1 - bytesRead), &bytes, nullptr);
|
||||
if (!readOk || bytes == 0) {
|
||||
FF_DEBUG("ReadFile() %s, bytes=%lu: %s", readOk ? "returned no data" : "failed", bytes, readOk ? "no error" : ffDebugWin32Error(GetLastError()));
|
||||
va_end(args);
|
||||
return "ReadFile() failed";
|
||||
}
|
||||
@@ -464,6 +494,7 @@ const char* ffGetTerminalResponse(const char* request, int nParams, const char*
|
||||
|
||||
if (ret <= 0) {
|
||||
va_end(args);
|
||||
FF_DEBUG("vsscanf(buffer, format, args) failed: %s", strerror(errno));
|
||||
return "vsscanf(buffer, format, args) failed";
|
||||
}
|
||||
if (ret >= nParams) {
|
||||
@@ -500,8 +531,19 @@ FFNativeFD ffGetNullFD(void) {
|
||||
return hNullFile;
|
||||
}
|
||||
|
||||
bool ffIsTerminal(int fd) {
|
||||
intptr_t handle = _get_osfhandle(fd);
|
||||
if (handle == -1 || handle == -2) {
|
||||
// -1: an invalid fd; -2: a valid fd with no file behind it (`_get_osfhandle`'s way of
|
||||
// saying FF_INVALID_FD). Neither is a terminal.
|
||||
return false;
|
||||
}
|
||||
DWORD mode;
|
||||
return GetConsoleMode((HANDLE) handle, &mode) != 0;
|
||||
}
|
||||
|
||||
bool ffRemoveFile(const char* fileName) {
|
||||
wchar_t fileNameW[MAX_PATH];
|
||||
wchar_t fileNameW[MAX_PATH]; // see the long path TODO at the top of this file
|
||||
ULONG len;
|
||||
if (!NT_SUCCESS(RtlUTF8ToUnicodeN(fileNameW, (ULONG) sizeof(fileNameW), &len, fileName, (ULONG) strlen(fileName) + 1))) {
|
||||
return false;
|
||||
@@ -513,7 +555,7 @@ bool ffRemoveFile(const char* fileName) {
|
||||
}
|
||||
|
||||
uint64_t ffPathGetMtime(const char* path) {
|
||||
wchar_t fileNameW[MAX_PATH];
|
||||
wchar_t fileNameW[MAX_PATH]; // see the long path TODO at the top of this file
|
||||
ULONG len;
|
||||
if (!NT_SUCCESS(RtlUTF8ToUnicodeN(fileNameW, (ULONG) sizeof(fileNameW), &len, path, (ULONG) strlen(path) + 1))) {
|
||||
return 0;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "fastfetch.h"
|
||||
#include "common/color.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/jsonconfig.h"
|
||||
#include "common/printing.h"
|
||||
#include "common/io.h"
|
||||
@@ -60,6 +61,22 @@ void ffJsonConfigGenerateModuleArgsConfig(yyjson_mut_doc* doc, yyjson_mut_val* m
|
||||
}
|
||||
}
|
||||
|
||||
bool ffJsonConfigParseUInt32(yyjson_val* val, uint32_t* result, uint32_t max) {
|
||||
if (!yyjson_is_int(val)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// `yyjson_get_sint()` is exact for every value that fits into `int64_t`, and is negative for the
|
||||
// `uint64_t` values above `INT64_MAX`, which no caller accepts anyway.
|
||||
int64_t num = unsafe_yyjson_get_sint(val);
|
||||
if (num < 0 || (uint64_t) num > max) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*result = (uint32_t) num;
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* ffJsonConfigParseEnum(yyjson_val* val, int* result, FFKeyValuePair pairs[]) {
|
||||
if (yyjson_is_int(val)) {
|
||||
int intVal = yyjson_get_int(val);
|
||||
@@ -71,6 +88,7 @@ const char* ffJsonConfigParseEnum(yyjson_val* val, int* result, FFKeyValuePair p
|
||||
}
|
||||
}
|
||||
|
||||
FF_DEBUG("The enum value %d is not one of the accepted integers", intVal);
|
||||
return "Invalid enum integer";
|
||||
} else if (yyjson_is_str(val)) {
|
||||
const char* strVal = yyjson_get_str(val);
|
||||
@@ -81,8 +99,11 @@ const char* ffJsonConfigParseEnum(yyjson_val* val, int* result, FFKeyValuePair p
|
||||
}
|
||||
}
|
||||
|
||||
FF_DEBUG("The enum string \"%s\" is not one of the accepted names", strVal);
|
||||
return "Invalid enum string";
|
||||
} else {
|
||||
FF_DEBUG("The enum value is of JSON type %s, not a string or an integer",
|
||||
yyjson_get_type_desc(val));
|
||||
return "Invalid enum value type; must be a string or integer";
|
||||
}
|
||||
}
|
||||
@@ -133,7 +154,10 @@ static bool parseModuleJsonObject(const char* type, yyjson_val* jsonVal, yyjson_
|
||||
|
||||
static void prepareModuleJsonObject(const char* type, yyjson_val* module) {
|
||||
switch (type[0]) {
|
||||
#if !FF_MODULE_DISABLE_CPUUSAGE
|
||||
// Both `cpuusage` and `command` start with a c, and they are disabled by separate options,
|
||||
// so the block has to survive either one being off -- otherwise a build with only the other
|
||||
// one enabled would never prepare this module (see the same guard in `commandoption.c`).
|
||||
#if !FF_MODULE_DISABLE_CPUUSAGE || !FF_MODULE_DISABLE_COMMAND
|
||||
case 'c':
|
||||
case 'C': {
|
||||
if (ffStrEqualsIgnCase(type, ffCPUUsageModuleInfo.name)) {
|
||||
@@ -254,6 +278,7 @@ static const char* printJsonConfig(FFdata* data, bool prepare) {
|
||||
assert(root);
|
||||
|
||||
if (!yyjson_is_obj(root)) {
|
||||
FF_DEBUG("The config root is of JSON type %s", yyjson_get_type_desc(root));
|
||||
return "Invalid JSON config format. Root value must be an object";
|
||||
}
|
||||
|
||||
@@ -262,6 +287,7 @@ static const char* printJsonConfig(FFdata* data, bool prepare) {
|
||||
return nullptr;
|
||||
}
|
||||
if (!yyjson_is_arr(modules)) {
|
||||
FF_DEBUG("'modules' is of JSON type %s", yyjson_get_type_desc(modules));
|
||||
return "Property 'modules' must be an array of strings or objects";
|
||||
}
|
||||
|
||||
@@ -276,6 +302,7 @@ static const char* printJsonConfig(FFdata* data, bool prepare) {
|
||||
}
|
||||
|
||||
yyjson_val* module = item;
|
||||
bool gatedOnSucceeded = false;
|
||||
const char* type = yyjson_get_str(module);
|
||||
if (type) {
|
||||
module = nullptr;
|
||||
@@ -283,6 +310,7 @@ static const char* printJsonConfig(FFdata* data, bool prepare) {
|
||||
yyjson_val* conditions = yyjson_obj_get(module, "condition");
|
||||
if (conditions) {
|
||||
if (!yyjson_is_obj(conditions)) {
|
||||
FF_DEBUG("'condition' is of JSON type %s", yyjson_get_type_desc(conditions));
|
||||
return "Property 'condition' must be an object";
|
||||
}
|
||||
|
||||
@@ -309,8 +337,43 @@ static const char* printJsonConfig(FFdata* data, bool prepare) {
|
||||
yyjson_val* previousSucceeded = yyjson_obj_get(conditions, "succeeded");
|
||||
if (previousSucceeded && !unsafe_yyjson_is_null(previousSucceeded)) {
|
||||
if (!unsafe_yyjson_is_bool(previousSucceeded)) {
|
||||
FF_DEBUG("'condition.succeeded' is of JSON type %s",
|
||||
yyjson_get_type_desc(previousSucceeded));
|
||||
return "Property 'succeeded' in 'condition' must be a boolean";
|
||||
}
|
||||
gatedOnSucceeded = true;
|
||||
|
||||
if (prepare) {
|
||||
// Whether this module is printed at all depends on the result of the previous
|
||||
// one, which is only known in the print pass. Preparing it here would leave a
|
||||
// result behind that no print pass ever consumes, and since the modules are
|
||||
// paired by position (see `ffPrepareCommand` / `ffDetectCommand`), that
|
||||
// would silently shift every later module onto the wrong result.
|
||||
// Skip it, and let the module report the missing preparation if it turns out
|
||||
// to be printed after all.
|
||||
continue;
|
||||
}
|
||||
|
||||
#if !FF_MODULE_DISABLE_COMMAND
|
||||
// A `command` module that runs in parallel is exactly the pairing above, so this
|
||||
// combination cannot be left to the print pass: the module would be handed the
|
||||
// result prepared for the next `command` module and print that instead of its
|
||||
// own output. Refused here rather than in the prepare pass, because returning
|
||||
// from there would leave every later module unprepared -- which the print pass
|
||||
// would then report as a failure of theirs.
|
||||
// `parallel` is on by default, so leaving the key out is the parallel case too.
|
||||
if (gatedOnSucceeded && ffStrEqualsIgnCase(yyjson_get_str(yyjson_obj_get(module, "type")), ffCommandModuleInfo.name)) {
|
||||
yyjson_val* parallel = yyjson_obj_get(module, "parallel");
|
||||
if (parallel == nullptr || yyjson_get_bool(parallel)) {
|
||||
FF_DEBUG("The command module at index %zu is gated on 'condition.succeeded' and runs in parallel", idx);
|
||||
return "Module \"command\" cannot be combined with \"condition.succeeded\" while it runs in "
|
||||
"parallel: whether it is printed is only known in the print pass, so it cannot be "
|
||||
"prepared, and the prepared results are matched to modules by position. Set "
|
||||
"\"parallel\": false on it, or drop the condition.";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (succeeded != unsafe_yyjson_get_bool(previousSucceeded)) {
|
||||
continue;
|
||||
}
|
||||
@@ -319,12 +382,14 @@ static const char* printJsonConfig(FFdata* data, bool prepare) {
|
||||
|
||||
type = yyjson_get_str(yyjson_obj_get(module, "type"));
|
||||
if (!type) {
|
||||
FF_DEBUG("The module object at index %zu has no \"type\" key, or it is not a string", idx);
|
||||
return "module object must contain a \"type\" key ( case sensitive )";
|
||||
}
|
||||
if (yyjson_obj_size(module) == 1) { // contains only Property type
|
||||
module = nullptr;
|
||||
}
|
||||
} else {
|
||||
FF_DEBUG("The modules entry at index %zu is of JSON type %s", idx, yyjson_get_type_desc(module));
|
||||
return "modules must be an array of strings or objects";
|
||||
}
|
||||
|
||||
@@ -365,6 +430,7 @@ static const char* printJsonConfig(FFdata* data, bool prepare) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
[[gnu::cold]]
|
||||
void ffPrintJsonConfig(FFdata* data, bool prepare) {
|
||||
yyjson_mut_doc* jsonDoc = data->resultDoc;
|
||||
const char* error = printJsonConfig(data, prepare);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#if FF_HAVE_LUA
|
||||
|
||||
#include "common/lua.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/mallocHelper.h"
|
||||
|
||||
struct FFLuaData luaData;
|
||||
@@ -141,6 +142,7 @@ static int yyjsonEncode(lua_State* L) {
|
||||
const char* ffLuaLoadState(void) {
|
||||
if (luaData.inited) {
|
||||
if (luaData.L == nullptr) {
|
||||
FF_DEBUG("The Lua state was already initialised and is gone");
|
||||
return "Lua library is not available";
|
||||
}
|
||||
return nullptr;
|
||||
@@ -212,6 +214,7 @@ const char* ffLuaLoadState(void) {
|
||||
|
||||
lua_State* L = ffluaL_newstate();
|
||||
if (L == nullptr) {
|
||||
FF_DEBUG("luaL_newstate() could not allocate an interpreter");
|
||||
return "luaL_newstate() failed";
|
||||
}
|
||||
#if LUA_VERSION_NUM >= 505
|
||||
|
||||
+39
-14
@@ -1,22 +1,47 @@
|
||||
#include "common/netif.h"
|
||||
#include "common/FFcache.h"
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <net/if.h>
|
||||
#include <netinet/in.h>
|
||||
#endif
|
||||
#include <string.h>
|
||||
|
||||
// netio, localip and dns resolve the default route on every round, and it can change while a
|
||||
// `--dynamic-interval` run is going on: a laptop switching from Wi-Fi to Ethernet, a VPN coming up.
|
||||
static FFNetifDefaultRouteResult defaultRouteV4;
|
||||
static FFNetifDefaultRouteResult defaultRouteV6;
|
||||
|
||||
static void resetDefaultRoute(void* storage) {
|
||||
// The impls only write the fields they found, so without this a round that finds no default
|
||||
// route would keep reporting the interface of the previous one.
|
||||
memset(storage, 0, sizeof(FFNetifDefaultRouteResult));
|
||||
}
|
||||
|
||||
static void initDefaultRouteV4(void* storage) {
|
||||
FFNetifDefaultRouteResult* result = storage;
|
||||
result->status = ffNetifGetDefaultRouteImplV4(result) ? FF_NETIF_OK : FF_NETIF_INVALID;
|
||||
}
|
||||
|
||||
static void initDefaultRouteV6(void* storage) {
|
||||
FFNetifDefaultRouteResult* result = storage;
|
||||
result->status = ffNetifGetDefaultRouteImplV6(result) ? FF_NETIF_OK : FF_NETIF_INVALID;
|
||||
}
|
||||
|
||||
static FFcacheEntry ffCacheEntryDefaultRouteV4 = {
|
||||
.name = "default route V4",
|
||||
.storage = &defaultRouteV4,
|
||||
.init = initDefaultRouteV4,
|
||||
.destroy = resetDefaultRoute,
|
||||
};
|
||||
|
||||
static FFcacheEntry ffCacheEntryDefaultRouteV6 = {
|
||||
.name = "default route V6",
|
||||
.storage = &defaultRouteV6,
|
||||
.init = initDefaultRouteV6,
|
||||
.destroy = resetDefaultRoute,
|
||||
};
|
||||
|
||||
const FFNetifDefaultRouteResult* ffNetifGetDefaultRouteV4(void) {
|
||||
static FFNetifDefaultRouteResult result;
|
||||
if (result.status == FF_NETIF_UNINITIALIZED) {
|
||||
result.status = ffNetifGetDefaultRouteImplV4(&result) ? FF_NETIF_OK : FF_NETIF_INVALID;
|
||||
}
|
||||
return &result;
|
||||
return ffCacheGet(&ffCacheEntryDefaultRouteV4);
|
||||
}
|
||||
|
||||
const FFNetifDefaultRouteResult* ffNetifGetDefaultRouteV6(void) {
|
||||
static FFNetifDefaultRouteResult result;
|
||||
if (result.status == FF_NETIF_UNINITIALIZED) {
|
||||
result.status = ffNetifGetDefaultRouteImplV6(&result) ? FF_NETIF_OK : FF_NETIF_INVALID;
|
||||
}
|
||||
return &result;
|
||||
return ffCacheGet(&ffCacheEntryDefaultRouteV6);
|
||||
}
|
||||
|
||||
@@ -1,197 +1,7 @@
|
||||
#include "fastfetch.h"
|
||||
#include "common/library.h"
|
||||
#include "common/networking.h"
|
||||
#include "common/strutil.h"
|
||||
#include "common/debug.h"
|
||||
|
||||
#ifdef FF_HAVE_ZLIB
|
||||
#include <zlib.h>
|
||||
|
||||
struct FFZlibLibrary {
|
||||
FF_LIBRARY_SYMBOL(inflateInit2_)
|
||||
FF_LIBRARY_SYMBOL(inflate)
|
||||
FF_LIBRARY_SYMBOL(inflateEnd)
|
||||
|
||||
bool inited;
|
||||
} zlibData;
|
||||
|
||||
const char* ffNetworkingLoadZlibLibrary(void) {
|
||||
if (!zlibData.inited) {
|
||||
zlibData.inited = true;
|
||||
FF_LIBRARY_LOAD_MESSAGE(zlib,
|
||||
#ifdef _WIN32
|
||||
"zlib1"
|
||||
#else
|
||||
"libz"
|
||||
#endif
|
||||
FF_LIBRARY_EXTENSION,
|
||||
2)
|
||||
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlib, zlibData, inflateInit2_)
|
||||
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlib, zlibData, inflate)
|
||||
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlib, zlibData, inflateEnd)
|
||||
zlib = nullptr; // don't auto dlclose
|
||||
}
|
||||
return zlibData.ffinflateEnd == nullptr ? "Failed to load libz" : nullptr;
|
||||
}
|
||||
|
||||
// Try to pre-read gzip header to determine uncompressed size
|
||||
static uint32_t guessGzipOutputSize(const void* data, uint32_t dataSize) {
|
||||
// gzip file format: http://www.zlib.org/rfc-gzip.html
|
||||
if (dataSize < 10 || ((const uint8_t*) data)[0] != 0x1f || ((const uint8_t*) data)[1] != 0x8b) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Uncompressed size in gzip format is stored in the last 4 bytes, but only valid if data is less than 4GB
|
||||
if (dataSize > 18) {
|
||||
// Get ISIZE value from the end of file (little endian)
|
||||
const uint8_t* tail = (const uint8_t*) data + dataSize - 4;
|
||||
uint32_t uncompressedSize = (uint32_t) tail[0] | ((uint32_t) tail[1] << 8u) | ((uint32_t) tail[2] << 16u) | ((uint32_t) tail[3] << 24u);
|
||||
|
||||
// For valid gzip files, this value is the length of the uncompressed data modulo 2^32
|
||||
if (uncompressedSize > 0) {
|
||||
FF_DEBUG("Read uncompressed size from GZIP trailer: %u bytes", uncompressedSize);
|
||||
// Add some margin to the estimated size for safety
|
||||
return uncompressedSize + 64;
|
||||
}
|
||||
}
|
||||
|
||||
// If unable to get size from trailer or size is 0, use estimated value
|
||||
// Typically, text data compression ratio is between 3-5x, we use the larger value
|
||||
uint32_t estimatedSize = dataSize * 5;
|
||||
FF_DEBUG("Unable to read exact uncompressed size, estimated as 5x of compressed data: %u bytes", estimatedSize);
|
||||
return estimatedSize;
|
||||
}
|
||||
|
||||
// Decompress gzip content
|
||||
bool ffNetworkingDecompressGzip(FFstrbuf* buffer, char* headerEnd) {
|
||||
// `headerEnd` itself is non-null per the `nonnull(2)` contract; what it points to is not expressible as an attribute
|
||||
assert(*headerEnd == '\r');
|
||||
|
||||
// Calculate header size
|
||||
uint32_t headerSize = (uint32_t) (headerEnd - buffer->chars);
|
||||
|
||||
*headerEnd = '\0'; // Replace delimiter with null character for easier processing
|
||||
// Ensure Content-Encoding is in response headers, not in response body
|
||||
bool hasGzip = strcasestr(buffer->chars, "\nContent-Encoding: gzip") != nullptr;
|
||||
*headerEnd = '\r'; // Restore delimiter
|
||||
|
||||
if (!hasGzip) {
|
||||
FF_DEBUG("No gzip compressed content detected, skipping decompression");
|
||||
return true;
|
||||
}
|
||||
|
||||
FF_DEBUG("Gzip compressed content detected, preparing for decompression");
|
||||
|
||||
const char* bodyStart = headerEnd + 4; // Skip delimiter
|
||||
|
||||
if (buffer->length <= headerSize + 4) {
|
||||
// No content to decompress
|
||||
FF_DEBUG("Compressed content size is 0, skipping decompression");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Calculate compressed content size
|
||||
uint32_t compressedSize = buffer->length - headerSize - 4;
|
||||
|
||||
// Check if content is actually in gzip format (gzip header magic is 0x1f 0x8b)
|
||||
if (compressedSize < 2 || (uint8_t) bodyStart[0] != 0x1f || (uint8_t) bodyStart[1] != 0x8b) {
|
||||
FF_DEBUG("Content is not valid gzip format, skipping decompression");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Predict uncompressed size
|
||||
uint32_t estimatedSize = guessGzipOutputSize(bodyStart, compressedSize);
|
||||
|
||||
// Create decompression buffer with estimated size
|
||||
FF_STRBUF_AUTO_DESTROY decompressedBuffer = ffStrbufCreateA(estimatedSize > 0 ? estimatedSize : compressedSize * 5);
|
||||
FF_DEBUG("Created decompression buffer: %u bytes", decompressedBuffer.allocated);
|
||||
|
||||
// Initialize decompression
|
||||
z_stream zs = {
|
||||
.zalloc = Z_NULL,
|
||||
.zfree = Z_NULL,
|
||||
.opaque = Z_NULL,
|
||||
.avail_in = (uInt) compressedSize,
|
||||
.next_in = (Bytef*) bodyStart,
|
||||
.avail_out = (uInt) ffStrbufGetFree(&decompressedBuffer),
|
||||
.next_out = (Bytef*) decompressedBuffer.chars,
|
||||
};
|
||||
|
||||
// Initialize decompression engine
|
||||
if (zlibData.ffinflateInit2_(&zs, 16 + MAX_WBITS, ZLIB_VERSION, (int) sizeof(z_stream)) != Z_OK) {
|
||||
FF_DEBUG("Failed to initialize decompression engine");
|
||||
return false;
|
||||
}
|
||||
uInt availableOut = zs.avail_out;
|
||||
|
||||
// Perform decompression
|
||||
int result = zlibData.ffinflate(&zs, Z_FINISH);
|
||||
|
||||
// If output buffer is insufficient, try to extend buffer
|
||||
while (result == Z_BUF_ERROR || (result != Z_STREAM_END && zs.avail_out == 0)) {
|
||||
FF_DEBUG("Output buffer insufficient, trying to extend");
|
||||
|
||||
// Save already decompressed data amount
|
||||
uint32_t alreadyDecompressed = (uint32_t) (availableOut - zs.avail_out);
|
||||
decompressedBuffer.length += alreadyDecompressed;
|
||||
decompressedBuffer.chars[decompressedBuffer.length] = '\0';
|
||||
|
||||
ffStrbufEnsureFree(&decompressedBuffer, decompressedBuffer.length / 2);
|
||||
|
||||
// Set output parameters to point to new buffer
|
||||
zs.avail_out = (uInt) ffStrbufGetFree(&decompressedBuffer);
|
||||
zs.next_out = (Bytef*) (decompressedBuffer.chars + decompressedBuffer.length);
|
||||
availableOut = zs.avail_out;
|
||||
|
||||
// Decompress again
|
||||
result = zlibData.ffinflate(&zs, Z_FINISH);
|
||||
}
|
||||
|
||||
// Check for decompression errors before using result
|
||||
if (result != Z_STREAM_END) {
|
||||
FF_DEBUG("Decompression failed with zlib error: %d", result);
|
||||
zlibData.ffinflateEnd(&zs);
|
||||
return false;
|
||||
}
|
||||
|
||||
zlibData.ffinflateEnd(&zs);
|
||||
|
||||
// Calculate decompressed size (from the last inflate call)
|
||||
uint32_t decompressedSize = (uint32_t) (availableOut - zs.avail_out);
|
||||
decompressedBuffer.length += decompressedSize;
|
||||
decompressedBuffer.chars[decompressedBuffer.length] = '\0';
|
||||
FF_DEBUG("Successfully decompressed %u bytes compressed data to %u bytes", compressedSize, decompressedBuffer.length);
|
||||
|
||||
// Modify Content-Length header and remove Content-Encoding header
|
||||
// Use decompressedBuffer.length (total) not decompressedSize (last chunk only)
|
||||
FF_STRBUF_AUTO_DESTROY newBuffer = ffStrbufCreateA(headerSize + decompressedBuffer.length + 64);
|
||||
|
||||
char* line = nullptr;
|
||||
size_t len = 0;
|
||||
while (ffStrbufGetline(&line, &len, buffer)) {
|
||||
if (ffStrStartsWithIgnCase(line, "Content-Encoding:")) {
|
||||
continue;
|
||||
} else if (ffStrStartsWithIgnCase(line, "Content-Length:")) {
|
||||
ffStrbufAppendF(&newBuffer, "Content-Length: %u\r\n", decompressedBuffer.length);
|
||||
continue;
|
||||
} else if (line[0] == '\r') {
|
||||
ffStrbufAppendS(&newBuffer, "\r\n");
|
||||
ffStrbufGetlineRestore(&line, &len, buffer);
|
||||
break;
|
||||
}
|
||||
|
||||
ffStrbufAppendS(&newBuffer, line); // Including the trailing \r
|
||||
ffStrbufAppendC(&newBuffer, '\n');
|
||||
}
|
||||
|
||||
ffStrbufAppend(&newBuffer, &decompressedBuffer);
|
||||
ffStrbufDestroy(buffer);
|
||||
ffStrbufInitMove(buffer, &newBuffer);
|
||||
|
||||
return true;
|
||||
}
|
||||
#endif // FF_HAVE_ZLIB
|
||||
|
||||
const char* ffNetworkingFindHeader(const char* headers, uint32_t headerEnd, const char* name, uint32_t* valueLen) {
|
||||
uint32_t nameLen = (uint32_t) strlen(name);
|
||||
uint32_t pos = 0;
|
||||
@@ -325,7 +135,12 @@ int ffNetworkingChunkedComplete(const char* body, uint32_t bodyLen, uint32_t* co
|
||||
// Keeps the status line and every header except `dropHeader` and `Content-Length`,
|
||||
// then emits a `Content-Length` matching the (already decoded) body.
|
||||
// `body` may point into `buffer->chars`; it is copied before `buffer` is released.
|
||||
static void rebuildResponse(FFstrbuf* buffer, uint32_t headerEnd, const char* body, uint32_t bodyLen, const char* dropHeader) {
|
||||
//
|
||||
// Returns the offset of the `\r` that opens the terminating CRLF CRLF of the new response. The
|
||||
// rebuild drops a header line and rewrites another, so the header it produces is shorter than the
|
||||
// one that went in: the offset the caller held before is no longer the one that describes this
|
||||
// buffer, and handing it on would point into the middle of the body.
|
||||
static uint32_t rebuildResponse(FFstrbuf* buffer, uint32_t headerEnd, const char* body, uint32_t bodyLen, const char* dropHeader) {
|
||||
FF_STRBUF_AUTO_DESTROY newBuffer = ffStrbufCreateA(headerEnd + bodyLen + 64);
|
||||
|
||||
uint32_t pos = 0;
|
||||
@@ -349,18 +164,24 @@ static void rebuildResponse(FFstrbuf* buffer, uint32_t headerEnd, const char* bo
|
||||
|
||||
ffStrbufDestroy(buffer);
|
||||
ffStrbufInitMove(buffer, &newBuffer);
|
||||
|
||||
// The terminating CRLF CRLF is the four bytes in front of the body
|
||||
return buffer->length - bodyLen - 4;
|
||||
}
|
||||
|
||||
bool ffNetworkingDecodeChunked(FFstrbuf* buffer, uint32_t headerEnd) {
|
||||
bool ffNetworkingDecodeChunked(FFstrbuf* buffer, uint32_t* headerEnd) {
|
||||
assert(buffer->allocated > 0);
|
||||
|
||||
// Held in a local because the rebuild below hands a different value back
|
||||
const uint32_t headerLength = *headerEnd;
|
||||
|
||||
// The header block is terminated by CR LF CR LF
|
||||
if (headerEnd + 4 > buffer->length) {
|
||||
if (headerLength + 4 > buffer->length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
char* body = buffer->chars + headerEnd + 4;
|
||||
uint32_t bodyLen = buffer->length - headerEnd - 4;
|
||||
char* body = buffer->chars + headerLength + 4;
|
||||
uint32_t bodyLen = buffer->length - headerLength - 4;
|
||||
|
||||
uint32_t consumed = 0;
|
||||
if (ffNetworkingChunkedComplete(body, bodyLen, &consumed) != 1) {
|
||||
@@ -391,6 +212,6 @@ bool ffNetworkingDecodeChunked(FFstrbuf* buffer, uint32_t headerEnd) {
|
||||
}
|
||||
|
||||
FF_DEBUG("Decoded chunked body: %u bytes encoded, %u bytes decoded", bodyLen, outLen);
|
||||
rebuildResponse(buffer, headerEnd, out, outLen, "Transfer-Encoding:");
|
||||
*headerEnd = rebuildResponse(buffer, headerLength, out, outLen, "Transfer-Encoding:");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -146,6 +146,7 @@ static const char* tryNonThreadingFastPath(FFNetworkingState* state) {
|
||||
#endif
|
||||
#else
|
||||
FF_UNUSED(state);
|
||||
FF_DEBUG("TFO support is not available");
|
||||
return "TFO support is not available";
|
||||
#endif
|
||||
}
|
||||
@@ -213,12 +214,6 @@ static const char* initNetworkingState(FFNetworkingState* state, const char* hos
|
||||
}
|
||||
ffStrbufAppendS(&state->command, "\r\nConnection: close\r\n"); // Explicitly tell the server we don't need to keep the connection
|
||||
|
||||
// If compression needs to be enabled
|
||||
if (state->compression) {
|
||||
FF_DEBUG("Enabling HTTP content compression");
|
||||
ffStrbufAppendS(&state->command, "Accept-Encoding: gzip\r\n");
|
||||
}
|
||||
|
||||
ffStrbufAppendS(&state->command, headers);
|
||||
ffStrbufAppendS(&state->command, "\r\n");
|
||||
|
||||
@@ -322,32 +317,14 @@ error:
|
||||
close(state->sockfd);
|
||||
state->sockfd = -1;
|
||||
}
|
||||
|
||||
ffStrbufClear(&state->command);
|
||||
return ret;
|
||||
}
|
||||
|
||||
const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* host, uint16_t port, const char* path, const char* headers) {
|
||||
FF_DEBUG("Preparing to send HTTP request: host=%s, port=%u, path=%s", host, port, path);
|
||||
|
||||
if (state->compression) {
|
||||
FF_DEBUG("Compression enabled, checking if zlib is available");
|
||||
|
||||
#ifdef FF_HAVE_ZLIB
|
||||
const char* zlibError = ffNetworkingLoadZlibLibrary();
|
||||
// Only enable compression if zlib library is successfully loaded
|
||||
if (zlibError == nullptr) {
|
||||
FF_DEBUG("Successfully loaded zlib library, compression enabled");
|
||||
} else {
|
||||
FF_DEBUG("Failed to load zlib library, compression disabled: %s", zlibError);
|
||||
state->compression = false;
|
||||
}
|
||||
#else
|
||||
FF_DEBUG("zlib not supported at build time, compression disabled");
|
||||
state->compression = false;
|
||||
#endif
|
||||
} else {
|
||||
FF_DEBUG("Compression disabled");
|
||||
}
|
||||
|
||||
const char* initResult = initNetworkingState(state, host, port, path, headers);
|
||||
if (initResult != nullptr) {
|
||||
FF_DEBUG("Initialization failed: %s", initResult);
|
||||
@@ -464,7 +441,10 @@ const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buf
|
||||
return "Response too large";
|
||||
}
|
||||
FF_DEBUG("Receive buffer is full, extending it");
|
||||
ffStrbufEnsureFreeNoCheck(buffer, buffer->allocated);
|
||||
// Asking for exactly the room that is left under the cap, rather than for as much as
|
||||
// the buffer holds again: the allocation is rounded up to a power of two, so a doubling
|
||||
// from a size that is not one of those lands past the cap the check above just cleared.
|
||||
ffStrbufEnsureFreeNoCheck(buffer, FF_NETWORKING_MAX_RESPONSE_SIZE - buffer->length - 1);
|
||||
}
|
||||
|
||||
// When the remaining length is known, ask for exactly that much. MSG_WAITALL then
|
||||
@@ -587,7 +567,8 @@ const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buf
|
||||
return "No HTTP header end found";
|
||||
}
|
||||
|
||||
if (chunked && !ffNetworkingDecodeChunked(buffer, headerEnd)) {
|
||||
if (chunked && !ffNetworkingDecodeChunked(buffer, &headerEnd)) {
|
||||
FF_DEBUG("Failed to decode chunked response");
|
||||
return "Failed to decode chunked response";
|
||||
}
|
||||
|
||||
@@ -597,23 +578,13 @@ const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buf
|
||||
}
|
||||
FF_DEBUG("Received valid HTTP 200 response, content %u bytes, total %u bytes", contentLength, buffer->length);
|
||||
|
||||
if (contentLength > 0 && buffer->length != contentLength + headerEnd + 4) {
|
||||
// A chunked response was framed by its last-chunk and has been rewritten around it, so any
|
||||
// `Content-Length` the server also sent describes a body that no longer exists. RFC 9112 6.3
|
||||
// says not to send both; when one does, the chunked framing is the one that was acted on.
|
||||
if (!chunked && contentLength > 0 && buffer->length != contentLength + headerEnd + 4) {
|
||||
FF_DEBUG("Received content length mismatches: %u != %u", buffer->length, contentLength + headerEnd + 4);
|
||||
return "Content length mismatch";
|
||||
}
|
||||
|
||||
// If compression was used, try to decompress
|
||||
#ifdef FF_HAVE_ZLIB
|
||||
if (state->compression) {
|
||||
FF_DEBUG("Content received, checking if compressed");
|
||||
if (!ffNetworkingDecompressGzip(buffer, buffer->chars + headerEnd)) {
|
||||
FF_DEBUG("Decompression failed or invalid compression format");
|
||||
return "Failed to decompress or invalid format";
|
||||
} else {
|
||||
FF_DEBUG("Decompression successful or no decompression needed, total length after decompression: %u bytes", buffer->length);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -14,8 +14,9 @@ static LPFN_CONNECTEX ConnectEx;
|
||||
|
||||
static const char* initWsaData(WSADATA* wsaData) {
|
||||
FF_DEBUG("Initializing WinSock");
|
||||
if (WSAStartup(MAKEWORD(2, 2), wsaData) != 0) {
|
||||
FF_DEBUG("WSAStartup() failed");
|
||||
int ret = WSAStartup(MAKEWORD(2, 2), wsaData);
|
||||
if (ret != 0) {
|
||||
FF_DEBUG("WSAStartup() failed: %s", ffDebugWin32Error((DWORD) ret));
|
||||
return "WSAStartup() failed";
|
||||
}
|
||||
|
||||
@@ -28,7 +29,7 @@ static const char* initWsaData(WSADATA* wsaData) {
|
||||
// Dummy socket needed for WSAIoctl
|
||||
SOCKET sockfd = WSASocketW(AF_INET, SOCK_STREAM, 0, nullptr, 0, 0);
|
||||
if (sockfd == INVALID_SOCKET) {
|
||||
FF_DEBUG("WSASocketW(AF_INET, SOCK_STREAM) failed");
|
||||
FF_DEBUG("WSASocketW(AF_INET, SOCK_STREAM) failed: %s", ffDebugWin32Error((DWORD) WSAGetLastError()));
|
||||
WSACleanup();
|
||||
return "WSASocketW(AF_INET, SOCK_STREAM) failed";
|
||||
}
|
||||
@@ -36,7 +37,7 @@ static const char* initWsaData(WSADATA* wsaData) {
|
||||
DWORD dwBytes;
|
||||
GUID guid = WSAID_CONNECTEX;
|
||||
if (WSAIoctl(sockfd, SIO_GET_EXTENSION_FUNCTION_POINTER, &guid, sizeof(guid), &ConnectEx, sizeof(ConnectEx), &dwBytes, nullptr, nullptr) != 0) {
|
||||
FF_DEBUG("WSAIoctl(sockfd, SIO_GET_EXTENSION_FUNCTION_POINTER) failed");
|
||||
FF_DEBUG("WSAIoctl(sockfd, SIO_GET_EXTENSION_FUNCTION_POINTER) failed: %s", ffDebugWin32Error((DWORD) WSAGetLastError()));
|
||||
closesocket(sockfd);
|
||||
WSACleanup();
|
||||
return "WSAIoctl(sockfd, SIO_GET_EXTENSION_FUNCTION_POINTER) failed";
|
||||
@@ -51,24 +52,6 @@ static const char* initWsaData(WSADATA* wsaData) {
|
||||
const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* host, uint16_t port, const char* path, const char* headers) {
|
||||
FF_DEBUG("Preparing to send HTTP request: host=%s, port=%u, path=%s", host, port, path);
|
||||
|
||||
if (state->compression) {
|
||||
#ifdef FF_HAVE_ZLIB
|
||||
const char* zlibError = ffNetworkingLoadZlibLibrary();
|
||||
// Only enable compression if zlib library is successfully loaded
|
||||
if (zlibError == nullptr) {
|
||||
FF_DEBUG("Successfully loaded zlib library, compression enabled");
|
||||
} else {
|
||||
FF_DEBUG("Failed to load zlib library, compression disabled: %s", zlibError);
|
||||
state->compression = false;
|
||||
}
|
||||
#else
|
||||
FF_DEBUG("zlib not supported at build time, compression disabled");
|
||||
state->compression = false;
|
||||
#endif
|
||||
} else {
|
||||
FF_DEBUG("Compression disabled");
|
||||
}
|
||||
|
||||
static WSADATA wsaData;
|
||||
if (wsaData.wVersion == 0) {
|
||||
const char* error = initWsaData(&wsaData);
|
||||
@@ -90,8 +73,9 @@ const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* ho
|
||||
};
|
||||
|
||||
wchar_t hostW[256];
|
||||
if (!NT_SUCCESS(RtlUTF8ToUnicodeN(hostW, (ULONG) sizeof(hostW), nullptr, host, (ULONG) strlen(host) + 1))) {
|
||||
FF_DEBUG("Failed to convert host to wide string: %s", host);
|
||||
NTSTATUS status = RtlUTF8ToUnicodeN(hostW, (ULONG) sizeof(hostW), nullptr, host, (ULONG) strlen(host) + 1);
|
||||
if (!NT_SUCCESS(status)) {
|
||||
FF_DEBUG("Failed to convert host to wide string: %s: %s", host, ffDebugNtStatus(status));
|
||||
return "Failed to convert host to wide string";
|
||||
}
|
||||
|
||||
@@ -99,14 +83,15 @@ const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* ho
|
||||
_itow(port, portW, 10);
|
||||
|
||||
FF_DEBUG("Resolving address: %s:%u (%s)", host, port, state->ipv6 ? "IPv6" : "IPv4");
|
||||
if (GetAddrInfoW(hostW, portW, &hints, &addr) != 0) {
|
||||
FF_DEBUG("GetAddrInfoW() failed");
|
||||
int ret = GetAddrInfoW(hostW, portW, &hints, &addr);
|
||||
if (ret != 0) {
|
||||
FF_DEBUG("GetAddrInfoW() failed: %s", ffDebugWin32Error((DWORD) ret));
|
||||
return "GetAddrInfoW() failed";
|
||||
}
|
||||
|
||||
state->sockfd = WSASocketW(addr->ai_family, addr->ai_socktype, addr->ai_protocol, nullptr, 0, 0);
|
||||
if (state->sockfd == INVALID_SOCKET) {
|
||||
FF_DEBUG("WSASocketW() failed");
|
||||
FF_DEBUG("WSASocketW() failed: %s", ffDebugWin32Error((DWORD) WSAGetLastError()));
|
||||
FreeAddrInfoW(addr);
|
||||
return "WSASocketW() failed";
|
||||
}
|
||||
@@ -149,8 +134,9 @@ const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* ho
|
||||
// Initialize overlapped structure with WSA event for asynchronous I/O
|
||||
state->overlapped = (OVERLAPPED) {};
|
||||
|
||||
if (!NT_SUCCESS(NtCreateEvent(&state->overlapped.hEvent, EVENT_ALL_ACCESS, nullptr, NotificationEvent, FALSE))) {
|
||||
FF_DEBUG("NtCreateEvent() failed");
|
||||
NTSTATUS eventStatus = NtCreateEvent(&state->overlapped.hEvent, EVENT_ALL_ACCESS, nullptr, NotificationEvent, FALSE);
|
||||
if (!NT_SUCCESS(eventStatus)) {
|
||||
FF_DEBUG("NtCreateEvent() failed: %s", ffDebugNtStatus(eventStatus));
|
||||
closesocket(state->sockfd);
|
||||
FreeAddrInfoW(addr);
|
||||
state->sockfd = INVALID_SOCKET;
|
||||
@@ -176,13 +162,6 @@ const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* ho
|
||||
ffStrbufAppendF(&state->command, ":%u", port);
|
||||
}
|
||||
ffStrbufAppendS(&state->command, "\r\nConnection: close\r\n"); // Explicitly request connection closure
|
||||
|
||||
// Add compression support if enabled
|
||||
if (state->compression) {
|
||||
FF_DEBUG("Enabling HTTP content compression");
|
||||
ffStrbufAppendS(&state->command, "Accept-Encoding: gzip\r\n");
|
||||
}
|
||||
|
||||
ffStrbufAppendS(&state->command, headers);
|
||||
ffStrbufAppendS(&state->command, "\r\n");
|
||||
|
||||
@@ -298,7 +277,10 @@ const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buf
|
||||
return "Response too large";
|
||||
}
|
||||
FF_DEBUG("Receive buffer is full, extending it");
|
||||
ffStrbufEnsureFreeNoCheck(buffer, buffer->allocated);
|
||||
// Asking for exactly the room that is left under the cap, rather than for as much as
|
||||
// the buffer holds again: the allocation is rounded up to a power of two, so a doubling
|
||||
// from a size that is not one of those lands past the cap the check above just cleared.
|
||||
ffStrbufEnsureFreeNoCheck(buffer, FF_NETWORKING_MAX_RESPONSE_SIZE - buffer->length - 1);
|
||||
}
|
||||
|
||||
FF_DEBUG("Data reception loop #%d, current buffer size: %u, available space: %u",
|
||||
@@ -413,7 +395,8 @@ const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buf
|
||||
return "No HTTP header end found";
|
||||
}
|
||||
|
||||
if (chunked && !ffNetworkingDecodeChunked(buffer, headerEnd)) {
|
||||
if (chunked && !ffNetworkingDecodeChunked(buffer, &headerEnd)) {
|
||||
FF_DEBUG("Failed to decode chunked response");
|
||||
return "Failed to decode chunked response";
|
||||
}
|
||||
|
||||
@@ -425,23 +408,13 @@ const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buf
|
||||
contentLength,
|
||||
buffer->length);
|
||||
|
||||
if (contentLength > 0 && buffer->length != contentLength + headerEnd + 4) {
|
||||
// A chunked response was framed by its last-chunk and has been rewritten around it, so any
|
||||
// `Content-Length` the server also sent describes a body that no longer exists. RFC 9112 6.3
|
||||
// says not to send both; when one does, the chunked framing is the one that was acted on.
|
||||
if (!chunked && contentLength > 0 && buffer->length != contentLength + headerEnd + 4) {
|
||||
FF_DEBUG("Received content length mismatches: %u != %u", buffer->length, contentLength + headerEnd + 4);
|
||||
return "Content length mismatch";
|
||||
}
|
||||
|
||||
// If compression was used, try to decompress
|
||||
#ifdef FF_HAVE_ZLIB
|
||||
if (state->compression) {
|
||||
FF_DEBUG("Content received, checking if compressed");
|
||||
if (!ffNetworkingDecompressGzip(buffer, buffer->chars + headerEnd)) {
|
||||
FF_DEBUG("Decompression failed or invalid compression format");
|
||||
return "Failed to decompress or invalid format";
|
||||
} else {
|
||||
FF_DEBUG("Decompression successful or no decompression needed, total length after decompression: %u bytes", buffer->length);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "common/path.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/io.h"
|
||||
#include "common/arrutil.h"
|
||||
|
||||
@@ -6,6 +7,7 @@
|
||||
const char* ffFindExecutableInPath(const char* name, FFstrbuf* result) {
|
||||
char* path = getenv("PATH");
|
||||
if (!path) {
|
||||
FF_DEBUG("The PATH environment variable is not set");
|
||||
return "$PATH not set";
|
||||
}
|
||||
|
||||
@@ -50,6 +52,7 @@ const char* ffFindExecutableInPath(const char* name, FFstrbuf* result) {
|
||||
return nullptr;
|
||||
}
|
||||
ffStrbufClear(result);
|
||||
FF_DEBUG("%s is not executable in any of the %s entries", name, "PATH");
|
||||
return "Executable not found";
|
||||
}
|
||||
#else
|
||||
@@ -62,6 +65,7 @@ const char* ffFindExecutableInPath(const char* name, FFstrbuf* result) {
|
||||
char buffer[MAX_PATH + 1];
|
||||
DWORD length = SearchPathA(nullptr, name, ".exe", sizeof(buffer), buffer, nullptr);
|
||||
if (length == 0) {
|
||||
FF_DEBUG("SearchPathA(%s) failed: %s", name, ffDebugWin32Error(GetLastError()));
|
||||
ffStrbufClear(result);
|
||||
return "Executable not found";
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "fastfetch.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/percent.h"
|
||||
#include "common/color.h"
|
||||
#include "common/option.h"
|
||||
@@ -27,6 +28,8 @@ const char* ffPercentParseTypeJsonConfig(yyjson_val* jsonVal, FFPercentageTypeFl
|
||||
yyjson_arr_foreach (jsonVal, idx, max, item) {
|
||||
const char* flag = yyjson_get_str(item);
|
||||
if (!flag) {
|
||||
FF_DEBUG("A percent.type array entry is of JSON type %s, not a string",
|
||||
yyjson_get_type_desc(item));
|
||||
return "Error: percent.type: invalid flag string";
|
||||
}
|
||||
if (ffStrEqualsIgnCase(flag, "num")) {
|
||||
@@ -40,6 +43,7 @@ const char* ffPercentParseTypeJsonConfig(yyjson_val* jsonVal, FFPercentageTypeFl
|
||||
} else if (ffStrEqualsIgnCase(flag, "bar-monochrome")) {
|
||||
flags |= FF_PERCENTAGE_TYPE_BAR_MONOCHROME_BIT;
|
||||
} else {
|
||||
FF_DEBUG("Unknown percent.type flag \"%s\"", flag);
|
||||
return "Error: percent.type: unknown flag string";
|
||||
}
|
||||
}
|
||||
@@ -48,6 +52,7 @@ const char* ffPercentParseTypeJsonConfig(yyjson_val* jsonVal, FFPercentageTypeFl
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FF_DEBUG("percent.type is of JSON type %s", yyjson_get_type_desc(jsonVal));
|
||||
return "Error: usage: percent.type must be a number or an array of strings";
|
||||
}
|
||||
|
||||
@@ -83,8 +88,11 @@ void ffPercentAppendBar(FFstrbuf* buffer, double percent, FFPercentageModuleConf
|
||||
|
||||
FFPercentageTypeFlags percentType = config.type == 0 ? options->percentType : config.type;
|
||||
|
||||
uint8_t blocksPercent = (uint8_t) (percent / 100.0 * options->barWidth + 0.5);
|
||||
assert(blocksPercent <= options->barWidth);
|
||||
uint8_t blocksPercent = 0;
|
||||
if (percent > 0) {
|
||||
double blocks = percent / 100.0 * options->barWidth + 0.5;
|
||||
blocksPercent = (uint8_t) (blocks >= options->barWidth ? options->barWidth : blocks);
|
||||
}
|
||||
|
||||
bool autoColorElapsed = ffStrbufIgnCaseEqualS(&options->barColorElapsed, "auto");
|
||||
|
||||
@@ -285,6 +293,7 @@ bool ffPercentParseJsonObject(yyjson_val* key, yyjson_val* value, FFPercentageMo
|
||||
return true;
|
||||
}
|
||||
|
||||
[[gnu::cold]]
|
||||
void ffPercentGenerateJsonConfig(yyjson_mut_doc* doc, yyjson_mut_val* module, FFPercentageModuleConfig config) {
|
||||
yyjson_mut_val* percent = yyjson_mut_obj_add_obj(doc, module, "percent");
|
||||
yyjson_mut_obj_add_uint(doc, percent, "green", config.green);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include "common/textModifier.h"
|
||||
#include "logo/logo.h"
|
||||
|
||||
void ffPrintLogoAndKey(const char* moduleName, uint8_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType) {
|
||||
void ffPrintLogoAndKey(const char* moduleName, uint32_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType) {
|
||||
ffLogoPrintLine();
|
||||
|
||||
// This is used by --set-keyless, in this case we want neither the module name nor the separator
|
||||
@@ -40,7 +40,7 @@ void ffPrintLogoAndKey(const char* moduleName, uint8_t moduleIndex, const FFModu
|
||||
fputs(moduleName, stdout);
|
||||
|
||||
if (moduleIndex > 0) {
|
||||
printf(" %hhu", moduleIndex);
|
||||
printf(" %u", moduleIndex);
|
||||
}
|
||||
} else {
|
||||
FF_STRBUF_AUTO_DESTROY key = ffStrbufCreate();
|
||||
@@ -82,7 +82,7 @@ void ffPrintLogoAndKey(const char* moduleName, uint8_t moduleIndex, const FFModu
|
||||
}
|
||||
}
|
||||
|
||||
bool ffPrintFormat(const char* moduleName, uint8_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, uint32_t numArgs, const FFformatarg* arguments) {
|
||||
bool ffPrintFormat(const char* moduleName, uint32_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, uint32_t numArgs, const FFformatarg* arguments) {
|
||||
FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
|
||||
bool success;
|
||||
if (__builtin_expect(moduleArgs != nullptr, 1)) {
|
||||
@@ -102,7 +102,7 @@ bool ffPrintFormat(const char* moduleName, uint8_t moduleIndex, const FFModuleAr
|
||||
return success;
|
||||
}
|
||||
|
||||
void ffPrintError(const char* moduleName, uint8_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, const char* message, ...) {
|
||||
void ffPrintError(const char* moduleName, uint32_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, const char* message, ...) {
|
||||
if (!instance.config.display.showErrors) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "fastfetch.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/processing.h"
|
||||
#include "common/io.h"
|
||||
#include "common/strutil.h"
|
||||
@@ -42,28 +43,39 @@ extern char** environ;
|
||||
|
||||
enum { FF_PIPE_BUFSIZ = 8192 };
|
||||
|
||||
static inline int ffPipe2(int* fds, int flags) {
|
||||
static inline int ffPipe2(int* readEnd, int* writeEnd, int flags) {
|
||||
int fds[2];
|
||||
#ifndef FF_HAVE_PIPE2
|
||||
if (pipe(fds) == -1) {
|
||||
return -1;
|
||||
}
|
||||
fcntl(fds[0], F_SETFL, fcntl(fds[0], F_GETFL) | flags);
|
||||
fcntl(fds[1], F_SETFL, fcntl(fds[1], F_GETFL) | flags);
|
||||
return 0;
|
||||
#else
|
||||
return pipe2(fds, flags);
|
||||
if (pipe2(fds, flags) == -1) {
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
*readEnd = fds[0];
|
||||
*writeEnd = fds[1];
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Not thread-safe
|
||||
const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinFd, FFProcessHandle* outHandle) {
|
||||
int pipes[2];
|
||||
if (ffPipe2(pipes, O_CLOEXEC) == -1) {
|
||||
const char* ffProcessSpawn(char* const argv[], FFProcessOutputType useOutput, FFNativeFD stdinFd, FFProcessHandle* outHandle) {
|
||||
FF_AUTO_CLOSE_FD int pipeRead = -1;
|
||||
FF_AUTO_CLOSE_FD int pipeWrite = -1;
|
||||
if (ffPipe2(&pipeRead, &pipeWrite, O_CLOEXEC) == -1) {
|
||||
FF_DEBUG("pipe() failed: %s", strerror(errno));
|
||||
return "pipe() failed";
|
||||
}
|
||||
|
||||
pid_t childPid = -1;
|
||||
int nullFile = ffGetNullFD();
|
||||
// Each stream is wired to the pipe when its bit is set and to the null device otherwise, so an
|
||||
// unselected stream can neither mix into the captured text nor fill the pipe and block the child.
|
||||
int stdoutFd = (useOutput & FF_PROCESS_OUTPUT_STDOUT_BIT) ? pipeWrite : nullFile;
|
||||
int stderrFd = (useOutput & FF_PROCESS_OUTPUT_STDERR_BIT) ? pipeWrite : nullFile;
|
||||
|
||||
#if !(__ANDROID__ || __OpenBSD__)
|
||||
|
||||
@@ -75,8 +87,8 @@ const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinF
|
||||
|
||||
posix_spawn_file_actions_t file_actions;
|
||||
posix_spawn_file_actions_init(&file_actions);
|
||||
posix_spawn_file_actions_adddup2(&file_actions, pipes[1], useStdErr ? STDERR_FILENO : STDOUT_FILENO);
|
||||
posix_spawn_file_actions_adddup2(&file_actions, nullFile, useStdErr ? STDOUT_FILENO : STDERR_FILENO);
|
||||
posix_spawn_file_actions_adddup2(&file_actions, stdoutFd, STDOUT_FILENO);
|
||||
posix_spawn_file_actions_adddup2(&file_actions, stderrFd, STDERR_FILENO);
|
||||
if (ffIsValidNativeFD(stdinFd)) {
|
||||
posix_spawn_file_actions_adddup2(&file_actions, stdinFd, STDIN_FILENO);
|
||||
}
|
||||
@@ -123,8 +135,7 @@ const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinF
|
||||
posix_spawn_file_actions_destroy(&file_actions);
|
||||
|
||||
if (ret != 0) {
|
||||
close(pipes[0]);
|
||||
close(pipes[1]);
|
||||
FF_DEBUG("posix_spawnp() failed: %s", strerror(ret));
|
||||
if (ret == ENOENT) {
|
||||
return "command not found";
|
||||
}
|
||||
@@ -136,15 +147,14 @@ const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinF
|
||||
// https://github.com/termux/termux-packages/issues/25369
|
||||
childPid = fork();
|
||||
if (childPid == -1) {
|
||||
close(pipes[0]);
|
||||
close(pipes[1]);
|
||||
FF_DEBUG("fork() failed: %s", strerror(errno));
|
||||
return "fork() failed";
|
||||
}
|
||||
|
||||
if (childPid == 0) {
|
||||
// Child process
|
||||
dup2(pipes[1], useStdErr ? STDERR_FILENO : STDOUT_FILENO);
|
||||
dup2(nullFile, useStdErr ? STDOUT_FILENO : STDERR_FILENO);
|
||||
dup2(stdoutFd, STDOUT_FILENO);
|
||||
dup2(stderrFd, STDERR_FILENO);
|
||||
if (ffIsValidNativeFD(stdinFd)) {
|
||||
dup2(stdinFd, STDIN_FILENO);
|
||||
}
|
||||
@@ -155,9 +165,9 @@ const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinF
|
||||
|
||||
#endif
|
||||
|
||||
close(pipes[1]);
|
||||
outHandle->pid = childPid;
|
||||
outHandle->pipeRead = pipes[0];
|
||||
outHandle->pipeRead = pipeRead;
|
||||
pipeRead = -1; // ownership moved into outHandle, which ffProcessReadOutput() lets go
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -177,6 +187,7 @@ const char* ffProcessReadOutput(FFProcessHandle* handle, FFstrbuf* buffer) {
|
||||
struct pollfd pollfd = { childPipeFd, POLLIN, 0 };
|
||||
int pollret = poll(&pollfd, 1, timeout);
|
||||
if (pollret == 0) {
|
||||
FF_DEBUG("poll(&pollfd, 1, timeout) timeout (try increasing --processing-timeout)");
|
||||
kill(childPid, SIGTERM);
|
||||
waitpid(childPid, nullptr, 0);
|
||||
return "poll(&pollfd, 1, timeout) timeout (try increasing --processing-timeout)";
|
||||
@@ -199,6 +210,7 @@ const char* ffProcessReadOutput(FFProcessHandle* handle, FFstrbuf* buffer) {
|
||||
return "child process exited abnormally";
|
||||
}
|
||||
if (WEXITSTATUS(stat_loc) == 127) {
|
||||
FF_DEBUG("command not found");
|
||||
return "command not found";
|
||||
}
|
||||
// We only handle 127 as an error. See `getTerminalVersionUrxvt` in `terminalshell.c`
|
||||
@@ -210,6 +222,7 @@ const char* ffProcessReadOutput(FFProcessHandle* handle, FFstrbuf* buffer) {
|
||||
}
|
||||
}
|
||||
|
||||
FF_DEBUG("read(childPipeFd, str, FF_PIPE_BUFSIZ) failed: %s", strerror(errno));
|
||||
return "read(childPipeFd, str, FF_PIPE_BUFSIZ) failed";
|
||||
}
|
||||
|
||||
@@ -475,6 +488,7 @@ void ffProcessGetInfoLinux(pid_t pid, FFstrbuf* processName, FFstrbuf* exe, cons
|
||||
|
||||
const char* ffProcessGetBasicInfoLinux(pid_t pid, FFstrbuf* name, pid_t* ppid, int32_t* tty) {
|
||||
if (pid <= 0) {
|
||||
FF_DEBUG("Invalid pid");
|
||||
return "Invalid pid";
|
||||
}
|
||||
|
||||
@@ -489,6 +503,7 @@ const char* ffProcessGetBasicInfoLinux(pid_t pid, FFstrbuf* name, pid_t* ppid, i
|
||||
char buf[PROC_FILE_BUFFSIZ];
|
||||
ssize_t nRead = ffReadFileData(procFilePath, sizeof(buf) - 1, buf);
|
||||
if (nRead <= 8) {
|
||||
FF_DEBUG("ffReadFileData(/proc/pid/stat, PROC_FILE_BUFFSIZ-1, buf) failed");
|
||||
return "ffReadFileData(/proc/pid/stat, PROC_FILE_BUFFSIZ-1, buf) failed";
|
||||
}
|
||||
buf[nRead] = '\0'; // pid (comm) state ppid pgrp session tty
|
||||
@@ -499,16 +514,19 @@ const char* ffProcessGetBasicInfoLinux(pid_t pid, FFstrbuf* name, pid_t* ppid, i
|
||||
// comm in `/proc/pid/stat` is not encoded, and may contain ' ', ')' or even `\n`
|
||||
const char* start = memchr(buf, '(', (size_t) nRead);
|
||||
if (!start) {
|
||||
FF_DEBUG("memchr(stat, '(') failed");
|
||||
return "memchr(stat, '(') failed";
|
||||
}
|
||||
start++;
|
||||
const char* end = memrchr(start, ')', (size_t) nRead - (size_t) (start - buf));
|
||||
if (!end) {
|
||||
FF_DEBUG("memrchr(stat, ')') failed");
|
||||
return "memrchr(stat, ')') failed";
|
||||
}
|
||||
ffStrbufSetNS(name, (uint32_t) (end - start), start);
|
||||
ffStrbufTrimRightSpace(name);
|
||||
if (name->chars[0] == '\0') {
|
||||
FF_DEBUG("process name is empty");
|
||||
return "process name is empty";
|
||||
}
|
||||
pState = end + 2; // skip ") "
|
||||
@@ -520,6 +538,7 @@ const char* ffProcessGetBasicInfoLinux(pid_t pid, FFstrbuf* name, pid_t* ppid, i
|
||||
{
|
||||
int ppid_, tty_;
|
||||
if (sscanf(pState + 2, "%d %*d %*d %d", &ppid_, &tty_) < 2) {
|
||||
FF_DEBUG("sscanf(stat) failed");
|
||||
return "sscanf(stat) failed";
|
||||
}
|
||||
|
||||
@@ -536,6 +555,7 @@ const char* ffProcessGetBasicInfoLinux(pid_t pid, FFstrbuf* name, pid_t* ppid, i
|
||||
snprintf(procFilePath, sizeof(procFilePath), "/proc/%d/comm", (int) pid);
|
||||
ssize_t nRead = ffReadFileBuffer(procFilePath, name);
|
||||
if (nRead <= 0) {
|
||||
FF_DEBUG("ffReadFileBuffer(/proc/pid/comm, name) failed");
|
||||
return "ffReadFileBuffer(/proc/pid/comm, name) failed";
|
||||
}
|
||||
ffStrbufTrimRightSpace(name);
|
||||
@@ -548,6 +568,7 @@ const char* ffProcessGetBasicInfoLinux(pid_t pid, FFstrbuf* name, pid_t* ppid, i
|
||||
size_t size = sizeof(proc);
|
||||
if (sysctl(
|
||||
(int[]) { CTL_KERN, KERN_PROC, KERN_PROC_PID, pid }, 4, &proc, &size, nullptr, 0)) {
|
||||
FF_DEBUG("sysctl(KERN_PROC_PID) failed: %s", strerror(errno));
|
||||
return "sysctl(KERN_PROC_PID) failed";
|
||||
}
|
||||
|
||||
@@ -574,6 +595,7 @@ const char* ffProcessGetBasicInfoLinux(pid_t pid, FFstrbuf* name, pid_t* ppid, i
|
||||
size_t size = sizeof(proc);
|
||||
if (sysctl(
|
||||
(int[]) { CTL_KERN, KERN_PROC, KERN_PROC_PID, pid }, 4, &proc, &size, nullptr, 0)) {
|
||||
FF_DEBUG("sysctl(KERN_PROC_PID) failed: %s", strerror(errno));
|
||||
return "sysctl(KERN_PROC_PID) failed";
|
||||
}
|
||||
|
||||
@@ -600,6 +622,7 @@ const char* ffProcessGetBasicInfoLinux(pid_t pid, FFstrbuf* name, pid_t* ppid, i
|
||||
size_t size = sizeof(proc);
|
||||
if (sysctl(
|
||||
(int[]) { CTL_KERN, KERN_PROC2, KERN_PROC_PID, pid, sizeof(proc), 1 }, 6, &proc, &size, nullptr, 0) != 0) {
|
||||
FF_DEBUG("sysctl(KERN_PROC_PID) failed: %s", strerror(errno));
|
||||
return "sysctl(KERN_PROC_PID) failed";
|
||||
}
|
||||
|
||||
@@ -625,6 +648,7 @@ const char* ffProcessGetBasicInfoLinux(pid_t pid, FFstrbuf* name, pid_t* ppid, i
|
||||
snprintf(path, sizeof(path), "/proc/%d/psinfo", (int) pid);
|
||||
psinfo_t proc;
|
||||
if (ffReadFileData(path, sizeof(proc), &proc) != sizeof(proc)) {
|
||||
FF_DEBUG("ffReadFileData(psinfo) failed");
|
||||
return "ffReadFileData(psinfo) failed";
|
||||
}
|
||||
|
||||
@@ -650,6 +674,7 @@ const char* ffProcessGetBasicInfoLinux(pid_t pid, FFstrbuf* name, pid_t* ppid, i
|
||||
*tty = (int) proc.p_tdev;
|
||||
}
|
||||
} else {
|
||||
FF_DEBUG("sysctl(KERN_PROC_PID) failed: %s", strerror(errno));
|
||||
return "sysctl(KERN_PROC_PID) failed";
|
||||
}
|
||||
|
||||
@@ -667,6 +692,7 @@ const char* ffProcessGetBasicInfoLinux(pid_t pid, FFstrbuf* name, pid_t* ppid, i
|
||||
|
||||
#else
|
||||
|
||||
FF_DEBUG("Unsupported platform");
|
||||
return "Unsupported platform";
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "fastfetch.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/mallocHelper.h"
|
||||
#include "common/processing.h"
|
||||
#include "common/io.h"
|
||||
@@ -28,7 +29,7 @@ static void argvToCmdline(char* const argv[], FFstrbuf* result) {
|
||||
}
|
||||
|
||||
// Add quotes around string if whitespace chars are present (with slash duplicating at the end of string)
|
||||
if (ffStrbufFirstIndexS(&temp, " \t") != temp.length) {
|
||||
if (ffStrbufContainAnyC(&temp, " \t")) {
|
||||
uint32_t pos = temp.length;
|
||||
uint32_t cnt = 0;
|
||||
while (pos > 0 && temp.chars[pos - 1] == '\\') { ++cnt, --pos; }
|
||||
@@ -83,7 +84,7 @@ static wchar_t* createChildEnvironment(void) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinFd, FFProcessHandle* outHandle) {
|
||||
const char* ffProcessSpawn(char* const argv[], FFProcessOutputType useOutput, FFNativeFD stdinFd, FFProcessHandle* outHandle) {
|
||||
const int32_t timeout = instance.config.general.processingTimeout;
|
||||
|
||||
wchar_t pipeName[32];
|
||||
@@ -100,6 +101,7 @@ const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinF
|
||||
0,
|
||||
nullptr);
|
||||
if (hChildPipeRead == INVALID_HANDLE_VALUE) {
|
||||
FF_DEBUG("CreateNamedPipeW() failed: %s", ffDebugWin32Error(GetLastError()));
|
||||
return "CreateNamedPipeW(L\"\\\\.\\pipe\\FASTFETCH-$(PID)\") failed";
|
||||
}
|
||||
|
||||
@@ -116,6 +118,7 @@ const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinF
|
||||
0,
|
||||
nullptr);
|
||||
if (hChildPipeWrite == INVALID_HANDLE_VALUE) {
|
||||
FF_DEBUG("CreateFileW() failed: %s", ffDebugWin32Error(GetLastError()));
|
||||
return "CreateFileW(L\"\\\\.\\pipe\\FASTFETCH-$(PID)\") failed";
|
||||
}
|
||||
|
||||
@@ -124,13 +127,12 @@ const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinF
|
||||
.cb = sizeof(siStartInfo),
|
||||
.dwFlags = STARTF_USESTDHANDLES,
|
||||
};
|
||||
if (useStdErr) {
|
||||
siStartInfo.hStdOutput = ffGetNullFD();
|
||||
siStartInfo.hStdError = hChildPipeWrite;
|
||||
} else {
|
||||
siStartInfo.hStdOutput = hChildPipeWrite;
|
||||
siStartInfo.hStdError = ffGetNullFD();
|
||||
}
|
||||
|
||||
HANDLE nullFile = ffGetNullFD();
|
||||
// A stream wired to the pipe is captured; the other goes to the null device so it cannot mix
|
||||
// into the captured text or fill the pipe and block the child. Both may share the pipe handle.
|
||||
siStartInfo.hStdOutput = (useOutput & FF_PROCESS_OUTPUT_STDOUT_BIT) ? hChildPipeWrite : nullFile;
|
||||
siStartInfo.hStdError = (useOutput & FF_PROCESS_OUTPUT_STDERR_BIT) ? hChildPipeWrite : nullFile;
|
||||
if (ffIsValidNativeFD(stdinFd)) {
|
||||
siStartInfo.hStdInput = stdinFd;
|
||||
}
|
||||
@@ -139,9 +141,12 @@ const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinF
|
||||
{
|
||||
FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
|
||||
argvToCmdline(argv, &buf);
|
||||
FF_DEBUG("Command line: %s", buf.chars);
|
||||
uint32_t cmdlineBytes = (buf.length + 1) * sizeof(wchar_t);
|
||||
cmdline = malloc(cmdlineBytes);
|
||||
if (!NT_SUCCESS(RtlUTF8ToUnicodeN(cmdline, cmdlineBytes, nullptr, buf.chars, buf.length + 1))) {
|
||||
NTSTATUS status = RtlUTF8ToUnicodeN(cmdline, cmdlineBytes, nullptr, buf.chars, buf.length + 1);
|
||||
if (!NT_SUCCESS(status)) {
|
||||
FF_DEBUG("RtlUTF8ToUnicodeN() failed: %s", ffDebugNtStatus(status));
|
||||
return "RtlUTF8ToUnicodeN() failed";
|
||||
}
|
||||
}
|
||||
@@ -162,9 +167,12 @@ const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinF
|
||||
|
||||
NtClose(hChildPipeWrite);
|
||||
if (!success) {
|
||||
if (GetLastError() == ERROR_FILE_NOT_FOUND) {
|
||||
DWORD error = GetLastError();
|
||||
if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND) {
|
||||
FF_DEBUG("Command not found: %s (%s)", argv[0], ffDebugWin32Error(error));
|
||||
return "command not found";
|
||||
}
|
||||
FF_DEBUG("CreateProcessW(%s) failed: %s", argv[0], ffDebugWin32Error(error));
|
||||
return "CreateProcessW() failed";
|
||||
}
|
||||
|
||||
@@ -197,7 +205,9 @@ const char* ffProcessReadOutput(FFProcessHandle* handle, FFstrbuf* buffer) {
|
||||
handle->pid = INVALID_HANDLE_VALUE;
|
||||
handle->pipeRead = INVALID_HANDLE_VALUE;
|
||||
|
||||
if (timeout >= 0 && !NT_SUCCESS(NtCreateEvent(&hReadEvent, EVENT_ALL_ACCESS, nullptr, SynchronizationEvent, FALSE))) {
|
||||
NTSTATUS createStatus = STATUS_SUCCESS;
|
||||
if (timeout >= 0 && !NT_SUCCESS(createStatus = NtCreateEvent(&hReadEvent, EVENT_ALL_ACCESS, nullptr, SynchronizationEvent, FALSE))) {
|
||||
FF_DEBUG("NtCreateEvent() failed: %s", ffDebugNtStatus(createStatus));
|
||||
return "NtCreateEvent() failed";
|
||||
}
|
||||
|
||||
@@ -216,7 +226,8 @@ const char* ffProcessReadOutput(FFProcessHandle* handle, FFstrbuf* buffer) {
|
||||
nullptr,
|
||||
nullptr);
|
||||
if (status == STATUS_PENDING) {
|
||||
switch (NtWaitForSingleObject(hReadEvent, FALSE, &(LARGE_INTEGER) { .QuadPart = (int64_t) timeout * -10000 })) {
|
||||
NTSTATUS waitStatus = NtWaitForSingleObject(hReadEvent, FALSE, &(LARGE_INTEGER) { .QuadPart = (int64_t) timeout * -10000 });
|
||||
switch (waitStatus) {
|
||||
case STATUS_WAIT_0:
|
||||
status = iosb.Status;
|
||||
break;
|
||||
@@ -227,6 +238,7 @@ const char* ffProcessReadOutput(FFProcessHandle* handle, FFstrbuf* buffer) {
|
||||
}
|
||||
|
||||
default:
|
||||
FF_DEBUG("NtWaitForSingleObject(hReadEvent) failed: %s", ffDebugNtStatus(waitStatus));
|
||||
terminateChildProcess(hProcess, hChildPipeRead, hReadEvent, &iosb);
|
||||
return "NtWaitForSingleObject(hReadEvent) failed";
|
||||
}
|
||||
@@ -237,6 +249,7 @@ const char* ffProcessReadOutput(FFProcessHandle* handle, FFstrbuf* buffer) {
|
||||
}
|
||||
|
||||
if (!NT_SUCCESS(status)) {
|
||||
FF_DEBUG("NtReadFile(hChildPipeRead) failed: %s", ffDebugNtStatus(status));
|
||||
terminateChildProcess(hProcess, hChildPipeRead, nullptr, &iosb);
|
||||
return "NtReadFile(hChildPipeRead) failed";
|
||||
}
|
||||
@@ -248,12 +261,21 @@ const char* ffProcessReadOutput(FFProcessHandle* handle, FFstrbuf* buffer) {
|
||||
exit: {
|
||||
PROCESS_BASIC_INFORMATION info = {};
|
||||
ULONG size;
|
||||
if (NT_SUCCESS(NtQueryInformationProcess(hProcess, ProcessBasicInformation, &info, sizeof(info), &size))) {
|
||||
NTSTATUS status = NtQueryInformationProcess(hProcess, ProcessBasicInformation, &info, sizeof(info), &size);
|
||||
if (NT_SUCCESS(status)) {
|
||||
assert(size == sizeof(info));
|
||||
if (info.ExitStatus != STILL_ACTIVE && info.ExitStatus != 0) {
|
||||
if (info.ExitStatus == 9009) {
|
||||
// Note: CMD swallows 9009 errorlevel and returns 1 by default.
|
||||
// One must use `& call exit %^ERRORLEVEL%` to propagate the correct error code.
|
||||
FF_DEBUG("Child process exited with error code 9009 (command not found)");
|
||||
return "command not found";
|
||||
}
|
||||
FF_DEBUG("Child process exited with an error: %x %s", (unsigned) info.ExitStatus, ffDebugNtStatus(info.ExitStatus));
|
||||
return "Child process exited with an error";
|
||||
}
|
||||
} else {
|
||||
FF_DEBUG("NtQueryInformationProcess(ProcessBasicInformation) failed: %s", ffDebugNtStatus(status));
|
||||
return "NtQueryInformationProcess(ProcessBasicInformation) failed";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
#include "common/sysctl.h"
|
||||
#include "common/debug.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef __OpenBSD__
|
||||
const char* ffSysctlGetString(int mib1, int mib2, FFstrbuf* result) {
|
||||
size_t neededLength;
|
||||
if (sysctl((int[]) { mib1, mib2 }, 2, nullptr, &neededLength, nullptr, 0) != 0 || neededLength == 1) { // neededLength is 1 for empty strings, because of the null terminator
|
||||
FF_DEBUG("sysctl({%d, %d}) length query failed: %s", mib1, mib2, strerror(errno));
|
||||
return "sysctl() length query failed";
|
||||
}
|
||||
|
||||
ffStrbufEnsureFree(result, (uint32_t) neededLength - 1);
|
||||
|
||||
if (sysctl((int[]) { mib1, mib2 }, 2, result->chars + result->length, &neededLength, nullptr, 0) != 0) {
|
||||
FF_DEBUG("sysctl({%d, %d}) failed to retrieve %zu bytes: %s", mib1, mib2, neededLength, strerror(errno));
|
||||
return "sysctl() failed to retrieve string data";
|
||||
}
|
||||
|
||||
@@ -62,12 +67,14 @@ int64_t ffSysctlGetInt64(int mib1, int mib2, int64_t defaultValue) {
|
||||
const char* ffSysctlGetString(const char* propName, FFstrbuf* result) {
|
||||
size_t neededLength;
|
||||
if (sysctlbyname(propName, nullptr, &neededLength, nullptr, 0) != 0 || neededLength == 1) { // neededLength is 1 for empty strings, because of the null terminator
|
||||
FF_DEBUG("sysctlbyname(%s) length query failed: %s", propName, strerror(errno));
|
||||
return "sysctlbyname() failed";
|
||||
}
|
||||
|
||||
ffStrbufEnsureFree(result, (uint32_t) neededLength - 1);
|
||||
|
||||
if (sysctlbyname(propName, result->chars + result->length, &neededLength, nullptr, 0) != 0) {
|
||||
FF_DEBUG("sysctlbyname(%s) failed to retrieve %zu bytes: %s", propName, neededLength, strerror(errno));
|
||||
return "sysctlbyname() failed to retrieve string data";
|
||||
}
|
||||
|
||||
|
||||
@@ -154,6 +154,7 @@ bool ffTempsParseJsonObject(yyjson_val* key, yyjson_val* value, bool* useTemp, F
|
||||
return true;
|
||||
}
|
||||
|
||||
[[gnu::cold]]
|
||||
void ffTempsGenerateJsonConfig(yyjson_mut_doc* doc, yyjson_mut_val* module, bool temp, FFColorRangeConfig config) {
|
||||
if (!temp) {
|
||||
yyjson_mut_obj_add_bool(doc, module, "temp", false);
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
#include "common/zlib.h"
|
||||
#include "common/debug.h"
|
||||
|
||||
#if FF_HAVE_ZLIB && !FF_DISABLE_DLOPEN
|
||||
|
||||
struct FFZlibData zlibData;
|
||||
|
||||
// Opens the library and leaves the handle in zlibData, so the groups that follow resolve against the
|
||||
// same mapping instead of opening their own.
|
||||
static const char* openZlib(void) {
|
||||
#ifdef _WIN32
|
||||
FF_LIBRARY_LOAD_MESSAGE(zlib, "zlib1" FF_LIBRARY_EXTENSION, 2)
|
||||
#else
|
||||
FF_LIBRARY_LOAD_MESSAGE(zlib, "libz" FF_LIBRARY_EXTENSION, 2)
|
||||
#endif
|
||||
|
||||
zlibData.handle = zlib;
|
||||
zlib = nullptr; // the handle is kept in zlibData, so the cleanup must not close it
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char* ffZlibLoad(FFZlibGroup group) {
|
||||
FF_DEBUG("Loading zlib group: %x", group);
|
||||
|
||||
// Opened once, by whichever group is asked for first; the later ones reuse that mapping. A
|
||||
// failed open is retried by the next group rather than cached, so a system without libz pays at
|
||||
// most one attempt per group -- not one per call.
|
||||
if (zlibData.handle == nullptr) {
|
||||
if (zlibData.attempted == UINT8_MAX) {
|
||||
return "Previous attempt to open zlib failed";
|
||||
}
|
||||
|
||||
const char* error = openZlib();
|
||||
if (error != nullptr) {
|
||||
static_assert(sizeof(zlibData.attempted) == 1, "zlibData.attempted must be 1 byte");
|
||||
zlibData.attempted = UINT8_MAX;
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
if (group & FF_ZLIB_COMPRESS) {
|
||||
if (zlibData.attempted & FF_ZLIB_COMPRESS) {
|
||||
if (!zlibData.ffcompress2) {
|
||||
return "Previous attempt to load compress failed";
|
||||
}
|
||||
} else {
|
||||
zlibData.attempted |= FF_ZLIB_COMPRESS;
|
||||
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlibData.handle, zlibData, compressBound)
|
||||
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlibData.handle, zlibData, compress2)
|
||||
}
|
||||
}
|
||||
if (group & FF_ZLIB_INFLATE) {
|
||||
if (zlibData.attempted & FF_ZLIB_INFLATE) {
|
||||
if (!zlibData.ffinflateEnd) {
|
||||
return "Previous attempt to load inflate failed";
|
||||
}
|
||||
} else {
|
||||
zlibData.attempted |= FF_ZLIB_INFLATE;
|
||||
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlibData.handle, zlibData, inflateInit2_)
|
||||
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlibData.handle, zlibData, inflate)
|
||||
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlibData.handle, zlibData, inflateEnd)
|
||||
}
|
||||
}
|
||||
|
||||
if (group & FF_ZLIB_GZFILE) {
|
||||
if (zlibData.attempted & FF_ZLIB_GZFILE) {
|
||||
if (!zlibData.ffgzclose) {
|
||||
return "Previous attempt to load gzfile failed";
|
||||
}
|
||||
} else {
|
||||
zlibData.attempted |= FF_ZLIB_GZFILE;
|
||||
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlibData.handle, zlibData, gzopen)
|
||||
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlibData.handle, zlibData, gzread)
|
||||
FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlibData.handle, zlibData, gzclose)
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
#endif
|
||||
+18
-5
@@ -132,12 +132,18 @@ static inline ssize_t ffReadFDData(FFNativeFD fd, size_t dataSize, void* data) {
|
||||
return ffReadFDData(fd, dataSize, data);
|
||||
}
|
||||
|
||||
// `CreateFileA` reads the path in the ANSI code page, so a UTF-8 path holding anything outside ASCII
|
||||
// names a different file, or none at all. The wide API is the one that takes what UTF-8 decodes to,
|
||||
// and the conversion is done the same way ffPathExists() below does it.
|
||||
[[gnu::nonnull(1, 2)]] static inline bool ffAppendFileBuffer(const char* fileName, FFstrbuf* buffer) {
|
||||
FF_AUTO_CLOSE_FD FFNativeFD fd =
|
||||
#ifndef _WIN32
|
||||
open(fileName, O_RDONLY | O_CLOEXEC);
|
||||
#ifdef _WIN32
|
||||
wchar_t fileNameW[MAX_PATH];
|
||||
if (!NT_SUCCESS(RtlUTF8ToUnicodeN(fileNameW, (ULONG) sizeof(fileNameW), nullptr, fileName, (ULONG) strlen(fileName) + 1))) {
|
||||
return false;
|
||||
}
|
||||
FF_AUTO_CLOSE_FD FFNativeFD fd = CreateFileW(fileNameW, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
#else
|
||||
CreateFileA(fileName, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
FF_AUTO_CLOSE_FD FFNativeFD fd = open(fileName, O_RDONLY | O_CLOEXEC);
|
||||
#endif
|
||||
|
||||
if (!ffIsValidNativeFD(fd)) {
|
||||
@@ -228,7 +234,7 @@ typedef enum FFPathType: uint8_t {
|
||||
|
||||
[[gnu::nonnull(1, 2)]] bool ffPathExpandEnv(const char* in, FFstrbuf* out);
|
||||
|
||||
#define FF_IO_TERM_RESP_WAIT_MS 200 // #554
|
||||
#define FF_IO_TERM_RESP_WAIT_MS 1000 // Terminal may respond slowly, especially when printing large image/gif logos. Found on iTerm
|
||||
|
||||
[[gnu::format(scanf, 3, 4), gnu::nonnull(1, 3)]] const char* ffGetTerminalResponse(const char* request, int nParams, const char* format, ...);
|
||||
|
||||
@@ -286,6 +292,13 @@ static inline void wrapClosedir(HANDLE* pdir) {
|
||||
}
|
||||
|
||||
FFNativeFD ffGetNullFD(void);
|
||||
// Whether a descriptor is attached to a real terminal, i.e. whether a human is looking at it.
|
||||
//
|
||||
// Do not use `isatty()` for this on Windows. It is `_isatty()`, which answers "is this a character
|
||||
// device" rather than "is this a console", and `NUL` -- as well as MSYS2's `/dev/null` -- is a
|
||||
// character device. A redirected run therefore looks interactive, which is how
|
||||
// `fastfetch --gen-config <path> > /dev/null` ends up on the interactive path and blocks forever.
|
||||
bool ffIsTerminal(int fd);
|
||||
// Returns whether the file was removed; callers that only want it gone discard that, so not `nodiscard`
|
||||
[[gnu::nonnull(1)]] bool ffRemoveFile(const char* fileName);
|
||||
// Modification time of a file, in milliseconds since the Unix epoch, or 0 if it can not be read.
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
// `val` may be null (yyjson predicates tolerate it); `result` and `pairs` may not.
|
||||
// Returns an error string, null on success, so it is `nodiscard`.
|
||||
[[gnu::nonnull(2, 3), nodiscard]] const char* ffJsonConfigParseEnum(yyjson_val* val, int* result, FFKeyValuePair pairs[]);
|
||||
// Reads an integer in `[0, max]` into `result`. Returns false for anything else.
|
||||
//
|
||||
// `yyjson_get_uint()` accepts the signed number subtype as well and reinterprets its bits, so the
|
||||
// bare `(uint32_t) yyjson_get_uint(val)` it replaces turned `-1` into `UINT32_MAX` — 4.3 billion
|
||||
// characters for `separator.times`, a ~50 day sleep for `waitTime`.
|
||||
[[gnu::nonnull(1, 2), nodiscard]] bool ffJsonConfigParseUInt32(yyjson_val* val, uint32_t* result, uint32_t max);
|
||||
|
||||
// The three helpers below are routinely called as statements, to add a key to a document, so their
|
||||
// result is not `nodiscard`.
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#define LUALIB_API LUA_API
|
||||
#define LUAMOD_API LUA_API
|
||||
|
||||
// Note: `#pragma GCC diagnostic push` is intentionally omitted here.
|
||||
#pragma GCC diagnostic ignored "-Wunused-function"
|
||||
#endif
|
||||
|
||||
|
||||
@@ -8,7 +8,9 @@ extern "C" {
|
||||
|
||||
// `memrchr` is a GNU extension and may not be declared by system headers even when the symbol exists.
|
||||
// Declare it unconditionally; the build system provides a fallback implementation when missing.
|
||||
#ifndef memrchr
|
||||
void* memrchr(const void* s, int c, size_t n);
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
+13
-7
@@ -25,7 +25,6 @@ typedef struct FFNetworkingState {
|
||||
FFstrbuf command;
|
||||
uint32_t timeout;
|
||||
bool ipv6;
|
||||
bool compression; // if true, HTTP content compression will be enabled if supported
|
||||
bool tfo; // if true, TCP Fast Open will be attempted first, and fallback to traditional connection if it fails
|
||||
} FFNetworkingState;
|
||||
|
||||
@@ -34,6 +33,15 @@ typedef struct FFNetworkingState {
|
||||
[[gnu::nonnull(1, 2, 4), nodiscard]] const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* host, uint16_t port, const char* path, const char* headers);
|
||||
[[gnu::nonnull(1, 2), nodiscard]] const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buffer);
|
||||
|
||||
// Releases what a finished request holds and returns `state` to the idle state, so that the next
|
||||
// `ffNetworkingSendHttpRequest` starts from scratch. A request that never reached the wire can
|
||||
// still own the request buffer, so a plain zeroing of the struct is not enough.
|
||||
// The caller stays responsible for its own bookkeeping, such as clearing a "request sent" latch.
|
||||
[[gnu::nonnull(1)]] static inline void ffNetworkingResetState(FFNetworkingState* state) {
|
||||
ffStrbufDestroy(&state->command);
|
||||
*state = (FFNetworkingState) {};
|
||||
}
|
||||
|
||||
// Case-insensitive header lookup restricted to the header block [0, headerEnd).
|
||||
// Restricting the range matters because the body may already share the same buffer.
|
||||
// Returns a pointer to the first character of the value; `valueLen` receives its
|
||||
@@ -51,7 +59,10 @@ typedef struct FFNetworkingState {
|
||||
|
||||
// Decodes a `Transfer-Encoding: chunked` body in place and rewrites the response with a
|
||||
// `Content-Length` header in place of `Transfer-Encoding`.
|
||||
[[gnu::nonnull(1), nodiscard]] bool ffNetworkingDecodeChunked(FFstrbuf* buffer, uint32_t headerEnd);
|
||||
// `headerEnd` is updated in place: the rewrite changes the header length, so the value the caller
|
||||
// passed in no longer describes the buffer afterwards. Callers must use the new one for anything
|
||||
// that addresses the body.
|
||||
[[gnu::nonnull(1, 2), nodiscard]] bool ffNetworkingDecodeChunked(FFstrbuf* buffer, uint32_t* headerEnd);
|
||||
|
||||
// Result of parsing a `Transfer-Encoding` header value
|
||||
typedef enum FFNetworkingTransferEncoding {
|
||||
@@ -66,8 +77,3 @@ typedef enum FFNetworkingTransferEncoding {
|
||||
// client cannot decode. Only a lone `chunked` is accepted; everything else is reported
|
||||
// as unsupported so that the caller fails the response instead of returning garbage.
|
||||
[[gnu::nonnull(1), gnu::pure, nodiscard]] FFNetworkingTransferEncoding ffNetworkingParseTransferEncoding(const char* value, uint32_t valueLen);
|
||||
|
||||
#ifdef FF_HAVE_ZLIB
|
||||
const char* ffNetworkingLoadZlibLibrary(void);
|
||||
[[gnu::nonnull(1, 2), nodiscard]] bool ffNetworkingDecompressGzip(FFstrbuf* buffer, char* headerEnd);
|
||||
#endif
|
||||
|
||||
+18
-17
@@ -21,23 +21,24 @@ typedef struct FFModuleFormatArgList {
|
||||
typedef struct FFModuleDisplayName {
|
||||
const char* en; // English
|
||||
|
||||
const char* ar; // Arabic
|
||||
const char* cs; // Czech
|
||||
const char* de; // German
|
||||
const char* es; // Spanish
|
||||
const char* fr; // French
|
||||
const char* gl; // Galician
|
||||
const char* he; // Hebrew
|
||||
const char* id; // Indonesian
|
||||
const char* it; // Italian
|
||||
const char* ja; // Japanese
|
||||
const char* ko; // Korean
|
||||
const char* pl; // Polish
|
||||
const char* pt; // (Brazilian) Portuguese
|
||||
const char* ru; // Russian
|
||||
const char* tr; // Turkish
|
||||
const char* uk; // Ukrainian
|
||||
const char* vi; // Vietnamese
|
||||
const char* ar; // Arabic
|
||||
const char* cs; // Czech
|
||||
const char* de; // German
|
||||
const char* es; // Spanish
|
||||
const char* fi; // Finnish
|
||||
const char* fr; // French
|
||||
const char* gl; // Galician
|
||||
const char* he; // Hebrew
|
||||
const char* id; // Indonesian
|
||||
const char* it; // Italian
|
||||
const char* ja; // Japanese
|
||||
const char* ko; // Korean
|
||||
const char* pl; // Polish
|
||||
const char* pt; // (Brazilian) Portuguese
|
||||
const char* ru; // Russian
|
||||
const char* tr; // Turkish
|
||||
const char* uk; // Ukrainian
|
||||
const char* vi; // Vietnamese
|
||||
const char* zh_CN; // Simplified Chinese
|
||||
const char* zh_TW; // Traditional Chinese
|
||||
} FFModuleDisplayName;
|
||||
|
||||
@@ -20,6 +20,14 @@ typedef struct FFPercentageModuleConfig {
|
||||
FFPercentageTypeFlags type;
|
||||
} FFPercentageModuleConfig;
|
||||
|
||||
// Maps a received signal strength in dBm onto the 0-100 scale the percentage formatters want.
|
||||
// -50 dBm and above counts as a perfect link, -100 dBm and below as no link at all, and the range in
|
||||
// between is linear. Both ends are clamped rather than extrapolated, so the result is always a
|
||||
// percentage -- which also keeps it usable with `{?x}` guards, unlike a raw negative RSSI.
|
||||
[[gnu::const]] static inline double ffRssiToSignalQuality(int32_t rssi) {
|
||||
return (double) (rssi >= -50 ? 100 : rssi <= -100 ? 0 : (rssi + 100) * 2);
|
||||
}
|
||||
|
||||
// if (green <= yellow)
|
||||
// [0, green]: print green
|
||||
// (green, yellow]: print yellow
|
||||
|
||||
@@ -14,14 +14,14 @@ typedef enum FFPrintType: uint8_t {
|
||||
// Both `moduleName` and `moduleArgs` are optional and explicitly handled: a null `moduleName` is what
|
||||
// `--set-keyless` relies on, and a null `moduleArgs` (used by ffPrintError callers) skips the custom
|
||||
// key entirely. So neither takes `nonnull`.
|
||||
void ffPrintLogoAndKey(const char* moduleName, uint8_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType);
|
||||
void ffPrintLogoAndKey(const char* moduleName, uint32_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType);
|
||||
// Same for `moduleArgs`; `numArgs` / `arguments` are only read when `moduleArgs` is non-null, so
|
||||
// `arguments` is not `nonnull(6)` either. At least one caller discards the result, so not `nodiscard`.
|
||||
bool ffPrintFormat(const char* moduleName, uint8_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, uint32_t numArgs, const FFformatarg* arguments);
|
||||
bool ffPrintFormat(const char* moduleName, uint32_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, uint32_t numArgs, const FFformatarg* arguments);
|
||||
#define FF_PRINT_FORMAT_CHECKED(moduleName, moduleIndex, moduleArgs, printType, arguments) \
|
||||
ffPrintFormat((moduleName), (moduleIndex), (moduleArgs), (printType), (sizeof(arguments) / sizeof(*arguments)), (arguments));
|
||||
// `moduleName` / `moduleArgs` are forwarded to ffPrintLogoAndKey, which accepts null for both;
|
||||
// only `message` is dereferenced here (by `vprintf`).
|
||||
[[gnu::format(printf, 5, 6), gnu::nonnull(5)]] void ffPrintError(const char* moduleName, uint8_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, const char* message, ...);
|
||||
[[gnu::format(printf, 5, 6), gnu::nonnull(5)]] void ffPrintError(const char* moduleName, uint32_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, const char* message, ...);
|
||||
[[gnu::nonnull(1)]] void ffPrintColor(const FFstrbuf* colorValue);
|
||||
void ffPrintCharTimes(char c, uint32_t times);
|
||||
|
||||
+14
-3
@@ -17,18 +17,29 @@ typedef struct FFProcessHandle {
|
||||
#endif
|
||||
} FFProcessHandle;
|
||||
|
||||
// Which of the child's two output streams are wired to the pipe that `ffProcessReadOutput()` drains.
|
||||
// The stream that is not selected is pointed at the null device, so it can neither interleave with
|
||||
// the captured text nor fill the pipe and block the child. This is a bitfield rather than a value
|
||||
// because `BOTH` is a combination: each backend sets up the child's two stdio slots independently,
|
||||
// testing one bit each.
|
||||
typedef enum FFProcessOutputType: uint8_t {
|
||||
FF_PROCESS_OUTPUT_STDOUT_BIT = 1 << 0,
|
||||
FF_PROCESS_OUTPUT_STDERR_BIT = 1 << 1,
|
||||
FF_PROCESS_OUTPUT_BOTH = FF_PROCESS_OUTPUT_STDOUT_BIT | FF_PROCESS_OUTPUT_STDERR_BIT,
|
||||
} FFProcessOutputType;
|
||||
|
||||
// `stdinFd` is the fd the child process gets as its stdin. Pass `ffGetNullFD()` to detach it from
|
||||
// our stdin, or `FF_PROCESS_INHERIT_STDIN` to inherit ours, which is what the helpers below do.
|
||||
// Detaching matters on Android, where `/system/bin/cmd` forwards its stdin over binder to the
|
||||
// service: the kernel rejects the whole transaction when that fd is a terminal, and the tool then
|
||||
// only reports `Failure calling service <name>: Failed transaction`.
|
||||
#define FF_PROCESS_INHERIT_STDIN ((FFNativeFD) -1)
|
||||
const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinFd, FFProcessHandle* outHandle);
|
||||
const char* ffProcessSpawn(char* const argv[], FFProcessOutputType useOutput, FFNativeFD stdinFd, FFProcessHandle* outHandle);
|
||||
const char* ffProcessReadOutput(FFProcessHandle* handle, FFstrbuf* buffer); // Destroys handle internally
|
||||
|
||||
static inline const char* ffProcessAppendStdOut(FFstrbuf* buffer, char* const argv[]) {
|
||||
FFProcessHandle handle;
|
||||
const char* error = ffProcessSpawn(argv, false, FF_PROCESS_INHERIT_STDIN, &handle);
|
||||
const char* error = ffProcessSpawn(argv, FF_PROCESS_OUTPUT_STDOUT_BIT, FF_PROCESS_INHERIT_STDIN, &handle);
|
||||
if (error) {
|
||||
return error;
|
||||
}
|
||||
@@ -42,7 +53,7 @@ static inline const char* ffProcessAppendStdOut(FFstrbuf* buffer, char* const ar
|
||||
|
||||
static inline const char* ffProcessAppendStdErr(FFstrbuf* buffer, char* const argv[]) {
|
||||
FFProcessHandle handle;
|
||||
const char* error = ffProcessSpawn(argv, true, FF_PROCESS_INHERIT_STDIN, &handle);
|
||||
const char* error = ffProcessSpawn(argv, FF_PROCESS_OUTPUT_STDERR_BIT, FF_PROCESS_INHERIT_STDIN, &handle);
|
||||
if (error) {
|
||||
return error;
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/FFstrbuf.h"
|
||||
#include "fastfetch.h"
|
||||
|
||||
// Not `pure`: it trims the trailing spaces of `value` before reporting whether it is set
|
||||
[[gnu::nonnull(1), nodiscard]] bool ffIsSmbiosValueSet(FFstrbuf* value);
|
||||
|
||||
@@ -1,10 +1,34 @@
|
||||
#!/bin/sh
|
||||
# Convert logo.svg to logo.ico
|
||||
rsvg-convert -w 16 -h 16 logo.svg > logo16.png
|
||||
rsvg-convert -w 32 -h 32 logo.svg > logo32.png
|
||||
rsvg-convert -w 48 -h 48 logo.svg > logo48.png
|
||||
rsvg-convert -w 64 -h 64 logo.svg > logo64.png
|
||||
rsvg-convert -w 128 -h 128 logo.svg > logo128.png
|
||||
rsvg-convert -w 256 -h 256 logo.svg > logo256.png
|
||||
convert logo16.png logo32.png logo48.png logo64.png logo128.png logo256.png logo.ico
|
||||
# Convert logo.svg to logo.ico.
|
||||
#
|
||||
# Every frame is stored as PNG. ImageMagick's ICO writer only uses PNG for frames that are
|
||||
# 256x256 or larger and falls back to an uncompressed DIB for everything smaller, which
|
||||
# inflates the icon from ~19 KB to ~108 KB (and to ~370 KB on ImageMagick 7, which also
|
||||
# stores 256x256 as a DIB). Packing the frames here keeps the icon small and predictable.
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
for size in 16 32 48 64 128 256; do
|
||||
rsvg-convert -w "$size" -h "$size" ../../logo/logo.svg > "logo$size.png"
|
||||
done
|
||||
|
||||
python3 - <<'PY'
|
||||
import struct
|
||||
|
||||
sizes = [16, 32, 48, 64, 128, 256]
|
||||
frames = [open(f"logo{s}.png", "rb").read() for s in sizes]
|
||||
|
||||
header = struct.pack("<HHH", 0, 1, len(sizes))
|
||||
offset = 6 + 16 * len(sizes)
|
||||
entries = b""
|
||||
for size, frame in zip(sizes, frames):
|
||||
dim = 0 if size == 256 else size # 0 means 256 in the ICO directory
|
||||
entries += struct.pack("<BBBBHHII", dim, dim, 0, 0, 1, 32, len(frame), offset)
|
||||
offset += len(frame)
|
||||
|
||||
with open("logo.ico", "wb") as f:
|
||||
f.write(header + entries + b"".join(frames))
|
||||
PY
|
||||
|
||||
rm logo16.png logo32.png logo48.png logo64.png logo128.png logo256.png
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "com.h"
|
||||
#include "common/debug.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
@@ -14,15 +15,19 @@ static const char* doInitCom() {
|
||||
if (FAILED(res)) {
|
||||
switch (res) {
|
||||
case E_INVALIDARG:
|
||||
FF_DEBUG("RoInitialize() failed: invalid argument: 0x%08lX (%s)", res, ffDebugHResult(res));
|
||||
return "RoInitialize() failed: invalid argument";
|
||||
case E_OUTOFMEMORY:
|
||||
FF_DEBUG("RoInitialize() failed: out of memory: 0x%08lX (%s)", res, ffDebugHResult(res));
|
||||
return "RoInitialize() failed: out of memory";
|
||||
case E_UNEXPECTED:
|
||||
FF_DEBUG("RoInitialize() failed: unexpected error: 0x%08lX (%s)", res, ffDebugHResult(res));
|
||||
return "RoInitialize() failed: unexpected error";
|
||||
case RPC_E_CHANGED_MODE:
|
||||
// COM was already initialized with a different concurrency model
|
||||
return nullptr;
|
||||
default:
|
||||
FF_DEBUG("RoInitialize() failed: unknown error: 0x%08lX (%s)", res, ffDebugHResult(res));
|
||||
return "RoInitialize() failed: unknown error";
|
||||
}
|
||||
}
|
||||
@@ -42,13 +47,16 @@ static const char* doInitCom() {
|
||||
if (FAILED(res)) {
|
||||
switch (res) {
|
||||
case E_INVALIDARG:
|
||||
FF_DEBUG("CoInitializeEx() failed: invalid argument: 0x%08lX (%s)", res, ffDebugHResult(res));
|
||||
return "CoInitializeEx() failed: invalid argument";
|
||||
case E_OUTOFMEMORY:
|
||||
FF_DEBUG("CoInitializeEx() failed: out of memory: 0x%08lX (%s)", res, ffDebugHResult(res));
|
||||
return "CoInitializeEx() failed: out of memory";
|
||||
case RPC_E_CHANGED_MODE:
|
||||
// COM was already initialized with a different concurrency model
|
||||
return nullptr;
|
||||
default:
|
||||
FF_DEBUG("CoInitializeEx() failed: unknown error: 0x%08lX (%s)", res, ffDebugHResult(res));
|
||||
return "CoInitializeEx() failed: unknown error";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
#include "folders.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/mallocHelper.h"
|
||||
#include "common/windows/nt.h"
|
||||
#include "common/windows/registry.h"
|
||||
#include "common/windows/unicode.h"
|
||||
|
||||
#include <ntstatus.h>
|
||||
|
||||
// Where the shell keeps the values SHGetKnownFolderPath() would compute.
|
||||
//
|
||||
// `User Shell Folders` is the store folder redirection writes to -- HKCU for the user's own
|
||||
// folders, HKLM for the machine-wide ones -- and it holds the *unexpanded* form, which is why the
|
||||
// environment expansion below is not optional. `ProfileList\<SID>\ProfileImagePath` is what the
|
||||
// logon session recorded as the profile directory, and is read by ffGetProfilePath().
|
||||
#define FF_FOLDERS_HKCU_SHELL_FOLDERS L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders"
|
||||
#define FF_FOLDERS_HKLM_SHELL_FOLDERS L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders"
|
||||
#define FF_FOLDERS_HKLM_CURRENT_VERSION L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion"
|
||||
#define FF_FOLDERS_HKLM_PROFILE_LIST L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList"
|
||||
|
||||
// The environment fallbacks. Two of the registry values above are self-referential -- `Common
|
||||
// AppData` is literally `%ProgramData%` -- so these are the same answer, not a different one.
|
||||
#define FF_FOLDERS_ENV_LOCAL_APP_DATA L"%LOCALAPPDATA%"
|
||||
#define FF_FOLDERS_ENV_ROAMING_APP_DATA L"%APPDATA%"
|
||||
#define FF_FOLDERS_ENV_PROGRAM_DATA L"%ProgramData%"
|
||||
#define FF_FOLDERS_ENV_PROGRAM_FILES L"%ProgramFiles%"
|
||||
|
||||
// Every folder is resolved once and then kept. ffGetKnownFolderPath() is called about a dozen times
|
||||
// per process -- getCacheDir, getConfigDirs and getDataDirs all ask for the same folders -- and
|
||||
// each answer is a pure function of the registry and this process' environment. `states` separates
|
||||
// "not looked up yet" from "looked up and not there", which the path itself cannot: an empty
|
||||
// FFstrbuf is a valid answer.
|
||||
//
|
||||
// No lock guards this: the only thread fastfetch ever creates is the networking module's
|
||||
// asynchronous connect, and no module that resolves a known folder runs on it.
|
||||
typedef enum FFFolderState : uint8_t {
|
||||
FF_FOLDER_UNRESOLVED,
|
||||
FF_FOLDER_FOUND,
|
||||
FF_FOLDER_MISSING,
|
||||
} FFFolderState;
|
||||
|
||||
static FFstrbuf folderPaths[FF_KNOWN_FOLDER_COUNT];
|
||||
static FFFolderState folderStates[FF_KNOWN_FOLDER_COUNT];
|
||||
|
||||
// Expands `%NAME%` placeholders from this process' environment block.
|
||||
//
|
||||
// The work is done in UTF-16 so that a user name outside the ANSI code page survives: getenv()
|
||||
// goes through the CRT's narrow copy of the environment and would mangle it.
|
||||
//
|
||||
// RtlExpandEnvironmentStrings() substitutes nothing and still reports success for a variable it
|
||||
// cannot find (measured), so a leftover '%' is treated as a failure. A directory name that
|
||||
// legitimately contains one is a false negative, and the only thing that costs is the caller's
|
||||
// fallback.
|
||||
static bool expandEnv(const wchar_t* source, uint32_t sourceLen, FFstrbuf* result) {
|
||||
// Asking for the required length is done with a null destination, which the call reports as
|
||||
// STATUS_BUFFER_TOO_SMALL -- a *failure* status that still fills in `size`. The status is
|
||||
// therefore deliberately not checked here; a call that writes nothing leaves `size` at zero.
|
||||
SIZE_T size = 0; // in characters, including the null terminator
|
||||
(void) RtlExpandEnvironmentStrings(nullptr, source, sourceLen, nullptr, 0, &size);
|
||||
if (size == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FF_AUTO_FREE wchar_t* expanded = (wchar_t*) malloc(size * sizeof(wchar_t));
|
||||
SIZE_T written = 0;
|
||||
if (!NT_SUCCESS(RtlExpandEnvironmentStrings(nullptr, source, sourceLen, expanded, size, &written)) || written == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (wcschr(expanded, L'%')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ffStrbufSetNWS(result, (uint32_t) (written - 1), expanded);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool expandEnvZ(const wchar_t* source, FFstrbuf* result) {
|
||||
return expandEnv(source, (uint32_t) wcslen(source), result);
|
||||
}
|
||||
|
||||
// `raw` is the payload of a REG_SZ / REG_EXPAND_SZ value: UTF-16, usually null terminated.
|
||||
static bool expandBuffer(const FFArgBuffer* raw, FFstrbuf* result) {
|
||||
uint32_t length = raw->length / sizeof(wchar_t);
|
||||
if (length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const wchar_t* valueW = (const wchar_t*) raw->data;
|
||||
if (valueW[length - 1] == L'\0') {
|
||||
--length;
|
||||
}
|
||||
|
||||
return length > 0 && expandEnv(valueW, length, result);
|
||||
}
|
||||
|
||||
// FF_ARG_TYPE_BUFFER rather than FF_ARG_TYPE_STRBUF, because the value has to stay UTF-16 for
|
||||
// expandEnv(); the strbuf flavour converts it to UTF-8 on the way out. The buffer flavour does not
|
||||
// report the registry type either, so REG_SZ and REG_EXPAND_SZ are not told apart -- expanding a
|
||||
// string without placeholders is a no-op, which is what makes that acceptable.
|
||||
static bool readExpandedValue(HANDLE hKey, const wchar_t* valueNameW, FFstrbuf* result) {
|
||||
FFArgBuffer raw = {};
|
||||
if (!ffRegReadData(hKey, valueNameW, &raw, nullptr)) {
|
||||
return false;
|
||||
}
|
||||
FF_AUTO_FREE void* data = raw.data;
|
||||
|
||||
return expandBuffer(&raw, result);
|
||||
}
|
||||
|
||||
static bool readExpandedValueFrom(HKEY hRootKey, const wchar_t* subKeyW, const wchar_t* valueNameW, FFstrbuf* result) {
|
||||
FF_AUTO_CLOSE_FD HANDLE hKey = nullptr;
|
||||
if (!ffRegOpenKeyForRead(hRootKey, subKeyW, &hKey, nullptr)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return readExpandedValue(hKey, valueNameW, result);
|
||||
}
|
||||
|
||||
// The SID comes from the caller rather than from a token query here: FFPlatform_windows.c already
|
||||
// resolves it for `platform->sid`, and the two are the same SID by definition.
|
||||
bool ffGetProfilePath(const wchar_t* sidW, FFstrbuf* result) {
|
||||
assert(sidW);
|
||||
|
||||
FF_AUTO_CLOSE_FD HANDLE hProfileList = nullptr;
|
||||
if (!ffRegOpenKeyForRead(HKEY_LOCAL_MACHINE, FF_FOLDERS_HKLM_PROFILE_LIST, &hProfileList, nullptr)) {
|
||||
FF_DEBUG("Cannot open %ls", FF_FOLDERS_HKLM_PROFILE_LIST);
|
||||
return false;
|
||||
}
|
||||
|
||||
FF_AUTO_CLOSE_FD HANDLE hProfile = nullptr;
|
||||
if (!ffRegOpenSubkeyForRead(hProfileList, sidW, &hProfile, nullptr)) {
|
||||
FF_DEBUG("No profile is registered for %ls", sidW);
|
||||
return false;
|
||||
}
|
||||
|
||||
return readExpandedValue(hProfile, L"ProfileImagePath", result);
|
||||
}
|
||||
|
||||
// `Local AppData` and `AppData` share a key, so they are read together.
|
||||
//
|
||||
// The names have to be exact, spaces and all: `Local AppData` is `Local`, one space, `AppData` --
|
||||
// there is no `Local App Data`, and asking for it fails the whole batch below.
|
||||
static const struct {
|
||||
FFKnownFolder folder;
|
||||
const wchar_t* valueNameW;
|
||||
const wchar_t* envW;
|
||||
} shellFolderEntries[] = {
|
||||
{ FF_KNOWN_FOLDER_LOCAL_APP_DATA, L"Local AppData", FF_FOLDERS_ENV_LOCAL_APP_DATA },
|
||||
{ FF_KNOWN_FOLDER_ROAMING_APP_DATA, L"AppData", FF_FOLDERS_ENV_ROAMING_APP_DATA },
|
||||
};
|
||||
|
||||
// One open plus one NtQueryMultipleValueKey resolves both -- and whichever of the two is asked for
|
||||
// first caches the pair, which is what the callers want anyway.
|
||||
//
|
||||
// NtQueryMultipleValueKey is all-or-nothing: one absent name fails the batch, which would push
|
||||
// *both* folders to the environment fallback. That is survivable, because these values are
|
||||
// themselves just `%LOCALAPPDATA%` and `%APPDATA%` expanded -- but it is also invisible, and an
|
||||
// invisible fallback is how a wrong value name went unnoticed here. So a failed batch is retried
|
||||
// one value at a time, and the extra queries are only paid when the batch has already failed.
|
||||
static void resolveUserShellFolders(void) {
|
||||
FFArgBuffer buffers[ARRAY_SIZE(shellFolderEntries)] = {};
|
||||
FFRegValueArg args[ARRAY_SIZE(shellFolderEntries)];
|
||||
for (uint32_t i = 0; i < ARRAY_SIZE(shellFolderEntries); ++i) {
|
||||
args[i] = (FFRegValueArg) { .type = FF_ARG_TYPE_BUFFER, .value = &buffers[i], .name = shellFolderEntries[i].valueNameW };
|
||||
}
|
||||
|
||||
FF_AUTO_CLOSE_FD HANDLE hKey = nullptr;
|
||||
if (ffRegOpenKeyForRead(HKEY_CURRENT_USER, FF_FOLDERS_HKCU_SHELL_FOLDERS, &hKey, nullptr)) {
|
||||
if (ffRegReadValues(hKey, ARRAY_SIZE(shellFolderEntries), args, nullptr)) {
|
||||
for (uint32_t i = 0; i < ARRAY_SIZE(shellFolderEntries); ++i) {
|
||||
if (expandBuffer(&buffers[i], &folderPaths[shellFolderEntries[i].folder])) {
|
||||
folderStates[shellFolderEntries[i].folder] = FF_FOLDER_FOUND;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
FF_DEBUG("Batched read of %ls failed; retrying one value at a time", FF_FOLDERS_HKCU_SHELL_FOLDERS);
|
||||
for (uint32_t i = 0; i < ARRAY_SIZE(shellFolderEntries); ++i) {
|
||||
if (readExpandedValue(hKey, shellFolderEntries[i].valueNameW, &folderPaths[shellFolderEntries[i].folder])) {
|
||||
folderStates[shellFolderEntries[i].folder] = FF_FOLDER_FOUND;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < ARRAY_SIZE(shellFolderEntries); ++i) {
|
||||
// Every buffer has to be freed even when the read failed part way through, which is why the
|
||||
// free is not tied to the success path above.
|
||||
free(buffers[i].data);
|
||||
|
||||
FFKnownFolder folder = shellFolderEntries[i].folder;
|
||||
if (folderStates[folder] == FF_FOLDER_UNRESOLVED &&
|
||||
expandEnvZ(shellFolderEntries[i].envW, &folderPaths[folder])) {
|
||||
folderStates[folder] = FF_FOLDER_FOUND;
|
||||
}
|
||||
if (folderStates[folder] == FF_FOLDER_UNRESOLVED) {
|
||||
folderStates[folder] = FF_FOLDER_MISSING;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool resolveFolder(FFKnownFolder folder) {
|
||||
switch (folder) {
|
||||
case FF_KNOWN_FOLDER_LOCAL_APP_DATA:
|
||||
case FF_KNOWN_FOLDER_ROAMING_APP_DATA:
|
||||
resolveUserShellFolders(); // Resolves both of them
|
||||
return folderStates[folder] == FF_FOLDER_FOUND;
|
||||
|
||||
case FF_KNOWN_FOLDER_PROGRAM_DATA:
|
||||
return readExpandedValueFrom(HKEY_LOCAL_MACHINE, FF_FOLDERS_HKLM_SHELL_FOLDERS, L"Common AppData", &folderPaths[folder]) ||
|
||||
expandEnvZ(FF_FOLDERS_ENV_PROGRAM_DATA, &folderPaths[folder]);
|
||||
|
||||
case FF_KNOWN_FOLDER_PROGRAM_FILES:
|
||||
return readExpandedValueFrom(HKEY_LOCAL_MACHINE, FF_FOLDERS_HKLM_CURRENT_VERSION, L"ProgramFilesDir", &folderPaths[folder]) ||
|
||||
expandEnvZ(FF_FOLDERS_ENV_PROGRAM_FILES, &folderPaths[folder]);
|
||||
|
||||
default:
|
||||
assert(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool ffGetKnownFolderPath(FFKnownFolder folder, FFstrbuf* result) {
|
||||
assert(result);
|
||||
assert(folder < FF_KNOWN_FOLDER_COUNT);
|
||||
|
||||
if (folderStates[folder] == FF_FOLDER_UNRESOLVED) {
|
||||
folderStates[folder] = resolveFolder(folder) ? FF_FOLDER_FOUND : FF_FOLDER_MISSING;
|
||||
}
|
||||
|
||||
if (folderStates[folder] != FF_FOLDER_FOUND) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ffStrbufSet(result, &folderPaths[folder]);
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include "fastfetch.h"
|
||||
#include "common/io.h"
|
||||
|
||||
#include <wchar.h> // ffGetProfilePath() takes a SID as a wide string
|
||||
|
||||
// The known folders fastfetch needs. They are spelled out instead of taking a REFKNOWNFOLDERID so
|
||||
// that this header does not have to drag in <shlobj.h> -- the call sites include it for reasons of
|
||||
// their own, and a header that only works when <windows.h> came first is a trap.
|
||||
typedef enum FFKnownFolder : uint8_t {
|
||||
FF_KNOWN_FOLDER_LOCAL_APP_DATA,
|
||||
FF_KNOWN_FOLDER_ROAMING_APP_DATA,
|
||||
FF_KNOWN_FOLDER_PROGRAM_DATA,
|
||||
FF_KNOWN_FOLDER_PROGRAM_FILES,
|
||||
FF_KNOWN_FOLDER_COUNT,
|
||||
} FFKnownFolder;
|
||||
|
||||
// Resolves a known folder without initializing the shell.
|
||||
//
|
||||
// The first SHGetKnownFolderPath() call in a process costs ~2.4 ms (measured on Windows 10 22H2):
|
||||
// the shell's known-folder machinery is built on demand, and whichever folder is asked for first
|
||||
// pays for it. Every later call is free, so a process that needs a handful of folders pays that
|
||||
// price once and can never amortize it. Each folder below is instead one registry value or one
|
||||
// environment variable away -- ~0.03 ms for all of them together -- so the shell is never touched.
|
||||
//
|
||||
// The answer is cached: these five folders are asked for about a dozen times per process, and each
|
||||
// answer is a pure function of the registry and this process' environment. That is safe without a
|
||||
// lock, because nothing that resolves a known folder ever runs on the one thread fastfetch creates.
|
||||
//
|
||||
// Returns false, leaving `result` untouched, when the value is missing or unusable, so that the
|
||||
// caller keeps whatever fallback it had for the shell API failing. The path uses backslashes,
|
||||
// exactly like SHGetKnownFolderPath().
|
||||
bool ffGetKnownFolderPath(FFKnownFolder folder, FFstrbuf* result);
|
||||
|
||||
// Resolves the profile directory recorded for `sidW`, a SID string as produced by
|
||||
// RtlConvertSidToUnicodeString().
|
||||
//
|
||||
// There is no FF_KNOWN_FOLDER_PROFILE because the caller already has the SID:
|
||||
// FFPlatform_windows.c resolves it once for `platform->sid`, and deriving it a second time here
|
||||
// would mean a second NtQueryInformationToken(). The answer is deliberately not cached -- this is
|
||||
// called once per process, and from one place.
|
||||
//
|
||||
// Returns false, leaving `result` untouched, when the value is missing or unusable, so that the
|
||||
// caller keeps whatever fallback it had for the shell API failing. The path uses backslashes,
|
||||
// exactly like SHGetKnownFolderPath().
|
||||
bool ffGetProfilePath(const wchar_t* sidW, FFstrbuf* result);
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 19 KiB |
@@ -1 +0,0 @@
|
||||
../../logo/logo.svg
|
||||
+24
-3
@@ -171,9 +171,6 @@ NTSTATUS NTAPI NtQuerySystemEnvironmentValueEx(
|
||||
_Out_opt_ PULONG Attributes // EFI_VARIABLE_*
|
||||
);
|
||||
|
||||
NTSTATUS NTAPI RtlGUIDFromString(IN PCUNICODE_STRING GuidString, OUT GUID* Guid);
|
||||
NTSTATUS NTAPI RtlStringFromGUIDEx(IN const GUID* Guid, OUT PUNICODE_STRING GuidString, IN BOOLEAN AllocateGuidString);
|
||||
|
||||
typedef struct _SYSTEM_SECUREBOOT_INFORMATION {
|
||||
BOOLEAN SecureBootEnabled;
|
||||
BOOLEAN SecureBootCapable;
|
||||
@@ -722,6 +719,10 @@ static inline bool ffIsSystemBasicProcessInfoAvailable() { // Includes HandleCou
|
||||
return SharedUserData->NtBuildNumber >= 26200; // MSDN says it was added in 26100.4770; ntdoc says 25H2
|
||||
}
|
||||
|
||||
static inline bool ffIsFileFsFullSizeInformationExAvailable() {
|
||||
return SharedUserData->NtBuildNumber >= 17763; // Windows 10 October 2018 Update (1809, REDSTONE5)
|
||||
}
|
||||
|
||||
NTSYSAPI NTSTATUS NTAPI NtOpenProcessToken(
|
||||
_In_ HANDLE ProcessHandle,
|
||||
_In_ ACCESS_MASK DesiredAccess,
|
||||
@@ -1422,3 +1423,23 @@ NTSYSAPI NTSTATUS NTAPI NtTerminateProcess(
|
||||
|
||||
NTSYSAPI NTSTATUS NTAPI RtlAcquirePebLock(VOID);
|
||||
NTSYSAPI NTSTATUS NTAPI RtlReleasePebLock(VOID);
|
||||
|
||||
typedef struct _FILE_FS_FULL_SIZE_INFORMATION_EX {
|
||||
ULONGLONG ActualTotalAllocationUnits;
|
||||
ULONGLONG ActualAvailableAllocationUnits;
|
||||
ULONGLONG ActualPoolUnavailableAllocationUnits;
|
||||
ULONGLONG CallerTotalAllocationUnits;
|
||||
ULONGLONG CallerAvailableAllocationUnits;
|
||||
ULONGLONG CallerPoolUnavailableAllocationUnits;
|
||||
ULONGLONG UsedAllocationUnits;
|
||||
ULONGLONG TotalReservedAllocationUnits;
|
||||
ULONGLONG VolumeStorageReserveAllocationUnits;
|
||||
ULONGLONG AvailableCommittedAllocationUnits;
|
||||
ULONGLONG PoolAvailableAllocationUnits;
|
||||
ULONG SectorsPerAllocationUnit;
|
||||
ULONG BytesPerSector;
|
||||
} FILE_FS_FULL_SIZE_INFORMATION_EX, *PFILE_FS_FULL_SIZE_INFORMATION_EX;
|
||||
|
||||
enum {
|
||||
FileFsFullSizeInformationEx = 14
|
||||
};
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
#include "winrt.hpp"
|
||||
|
||||
#if FF_HAVE_WINRT
|
||||
|
||||
#include <wchar.h>
|
||||
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
extern "C" [[noreturn]] void __cxa_pure_virtual(void) {
|
||||
__builtin_trap();
|
||||
}
|
||||
#endif
|
||||
|
||||
// C++/WinRT supplies the ABI vtable shape, including IUnknown and IInspectable. These concrete
|
||||
// classes only provide the implementation-specific state and methods; placement new keeps their
|
||||
// storage under the C allocator used by the rest of fastfetch.
|
||||
namespace {
|
||||
|
||||
using WinrtIterableAbi = abi_t<winrt::Windows::Foundation::Collections::IIterable<winrt::hstring>>;
|
||||
using WinrtIteratorAbi = abi_t<winrt::Windows::Foundation::Collections::IIterator<winrt::hstring>>;
|
||||
using WinrtHstringAbiOut = winrt::impl::arg_out<winrt::hstring>;
|
||||
|
||||
struct FFWinrtHstringIterable;
|
||||
struct FFWinrtHstringIterator;
|
||||
|
||||
struct FFWinrtHstringIterable final : WinrtIterableAbi {
|
||||
std::uint32_t refCount;
|
||||
FFlist strings; // HSTRING
|
||||
|
||||
std::int32_t STDMETHODCALLTYPE QueryInterface(winrt::guid const& riid, void** result) noexcept override;
|
||||
std::uint32_t STDMETHODCALLTYPE AddRef() noexcept override;
|
||||
std::uint32_t STDMETHODCALLTYPE Release() noexcept override;
|
||||
std::int32_t STDMETHODCALLTYPE GetIids(std::uint32_t* count, winrt::guid** iids) noexcept override;
|
||||
std::int32_t STDMETHODCALLTYPE GetRuntimeClassName(void** name) noexcept override;
|
||||
std::int32_t STDMETHODCALLTYPE GetTrustLevel(winrt::Windows::Foundation::TrustLevel* level) noexcept override;
|
||||
std::int32_t STDMETHODCALLTYPE First(void** result) noexcept override;
|
||||
};
|
||||
|
||||
struct FFWinrtHstringIterator final : WinrtIteratorAbi {
|
||||
std::uint32_t refCount;
|
||||
FFWinrtHstringIterable* owner;
|
||||
std::uint32_t index;
|
||||
|
||||
std::int32_t STDMETHODCALLTYPE QueryInterface(winrt::guid const& riid, void** result) noexcept override;
|
||||
std::uint32_t STDMETHODCALLTYPE AddRef() noexcept override;
|
||||
std::uint32_t STDMETHODCALLTYPE Release() noexcept override;
|
||||
std::int32_t STDMETHODCALLTYPE GetIids(std::uint32_t* count, winrt::guid** iids) noexcept override;
|
||||
std::int32_t STDMETHODCALLTYPE GetRuntimeClassName(void** name) noexcept override;
|
||||
std::int32_t STDMETHODCALLTYPE GetTrustLevel(winrt::Windows::Foundation::TrustLevel* level) noexcept override;
|
||||
std::int32_t STDMETHODCALLTYPE get_Current(WinrtHstringAbiOut value) noexcept override;
|
||||
std::int32_t STDMETHODCALLTYPE get_HasCurrent(bool* value) noexcept override;
|
||||
std::int32_t STDMETHODCALLTYPE MoveNext(bool* value) noexcept override;
|
||||
std::int32_t STDMETHODCALLTYPE GetMany(std::uint32_t capacity, WinrtHstringAbiOut values, std::uint32_t* actual) noexcept override;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// IIterable<HSTRING>
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
std::int32_t STDMETHODCALLTYPE FFWinrtHstringIterable::QueryInterface(winrt::guid const& riid, void** result) noexcept {
|
||||
if (!result) {
|
||||
return E_POINTER;
|
||||
}
|
||||
*result = nullptr;
|
||||
|
||||
if (riid == winrt::guid_of<winrt::Windows::Foundation::IUnknown>() ||
|
||||
riid == winrt::guid_of<winrt::Windows::Foundation::IInspectable>() ||
|
||||
riid == winrt::guid_of<winrt::Windows::Foundation::Collections::IIterable<winrt::hstring>>()) {
|
||||
*result = this;
|
||||
AddRef();
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
|
||||
std::uint32_t STDMETHODCALLTYPE FFWinrtHstringIterable::AddRef() noexcept {
|
||||
return ++refCount;
|
||||
}
|
||||
|
||||
std::uint32_t STDMETHODCALLTYPE FFWinrtHstringIterable::Release() noexcept {
|
||||
std::uint32_t remaining = --refCount;
|
||||
if (remaining == 0) {
|
||||
FF_LIST_FOR_EACH(HSTRING, string, strings) {
|
||||
WindowsDeleteString(*string);
|
||||
}
|
||||
ffListDestroy(&strings);
|
||||
this->~FFWinrtHstringIterable();
|
||||
free(this);
|
||||
}
|
||||
return remaining;
|
||||
}
|
||||
|
||||
std::int32_t STDMETHODCALLTYPE FFWinrtHstringIterable::GetIids(std::uint32_t* count, winrt::guid** iids) noexcept {
|
||||
*count = 0;
|
||||
*iids = nullptr;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
std::int32_t STDMETHODCALLTYPE FFWinrtHstringIterable::GetRuntimeClassName(void** name) noexcept {
|
||||
*name = nullptr;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// `level` is the ABI's 32-bit TrustLevel enum; BaseTrust is 0.
|
||||
std::int32_t STDMETHODCALLTYPE FFWinrtHstringIterable::GetTrustLevel(winrt::Windows::Foundation::TrustLevel* level) noexcept {
|
||||
*level = winrt::Windows::Foundation::TrustLevel::BaseTrust;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
std::int32_t STDMETHODCALLTYPE FFWinrtHstringIterable::First(void** result) noexcept {
|
||||
if (!result) {
|
||||
return E_POINTER;
|
||||
}
|
||||
*result = nullptr;
|
||||
|
||||
auto* iterator = (FFWinrtHstringIterator*) malloc(sizeof(FFWinrtHstringIterator));
|
||||
if (!iterator) {
|
||||
return E_OUTOFMEMORY;
|
||||
}
|
||||
|
||||
::new (iterator) FFWinrtHstringIterator();
|
||||
iterator->refCount = 1;
|
||||
iterator->owner = this;
|
||||
iterator->index = 0;
|
||||
|
||||
// The iterator outlives the call that created it, so it holds a reference of its own.
|
||||
AddRef();
|
||||
|
||||
*result = iterator;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// IIterator<HSTRING>
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
std::int32_t STDMETHODCALLTYPE FFWinrtHstringIterator::QueryInterface(winrt::guid const& riid, void** result) noexcept {
|
||||
if (!result) {
|
||||
return E_POINTER;
|
||||
}
|
||||
*result = nullptr;
|
||||
|
||||
if (riid == winrt::guid_of<winrt::Windows::Foundation::IUnknown>() ||
|
||||
riid == winrt::guid_of<winrt::Windows::Foundation::IInspectable>() ||
|
||||
riid == winrt::guid_of<winrt::Windows::Foundation::Collections::IIterator<winrt::hstring>>()) {
|
||||
*result = this;
|
||||
AddRef();
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
|
||||
std::uint32_t STDMETHODCALLTYPE FFWinrtHstringIterator::AddRef() noexcept {
|
||||
return ++refCount;
|
||||
}
|
||||
|
||||
std::uint32_t STDMETHODCALLTYPE FFWinrtHstringIterator::Release() noexcept {
|
||||
std::uint32_t remaining = --refCount;
|
||||
if (remaining == 0) {
|
||||
owner->Release();
|
||||
this->~FFWinrtHstringIterator();
|
||||
free(this);
|
||||
}
|
||||
return remaining;
|
||||
}
|
||||
|
||||
std::int32_t STDMETHODCALLTYPE FFWinrtHstringIterator::GetIids(std::uint32_t* count, winrt::guid** iids) noexcept {
|
||||
*count = 0;
|
||||
*iids = nullptr;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
std::int32_t STDMETHODCALLTYPE FFWinrtHstringIterator::GetRuntimeClassName(void** name) noexcept {
|
||||
*name = nullptr;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
std::int32_t STDMETHODCALLTYPE FFWinrtHstringIterator::GetTrustLevel(winrt::Windows::Foundation::TrustLevel* level) noexcept {
|
||||
*level = winrt::Windows::Foundation::TrustLevel::BaseTrust;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
std::int32_t STDMETHODCALLTYPE FFWinrtHstringIterator::get_Current(WinrtHstringAbiOut value) noexcept {
|
||||
if (!value) {
|
||||
return E_POINTER;
|
||||
}
|
||||
*value = nullptr;
|
||||
|
||||
if (index >= owner->strings.length) {
|
||||
return E_BOUNDS;
|
||||
}
|
||||
|
||||
// The caller owns what it gets, so hand out a duplicate rather than the stored string.
|
||||
return WindowsDuplicateString(*(HSTRING*) ffListGet(&owner->strings, sizeof(HSTRING), index), reinterpret_cast<HSTRING*>(value));
|
||||
}
|
||||
|
||||
std::int32_t STDMETHODCALLTYPE FFWinrtHstringIterator::get_HasCurrent(bool* value) noexcept {
|
||||
if (!value) {
|
||||
return E_POINTER;
|
||||
}
|
||||
*value = index < owner->strings.length;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
std::int32_t STDMETHODCALLTYPE FFWinrtHstringIterator::MoveNext(bool* value) noexcept {
|
||||
if (!value) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
if (index < owner->strings.length) {
|
||||
++index;
|
||||
}
|
||||
*value = index < owner->strings.length;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
std::int32_t STDMETHODCALLTYPE FFWinrtHstringIterator::GetMany(std::uint32_t capacity, WinrtHstringAbiOut values, std::uint32_t* actual) noexcept {
|
||||
if (!actual) {
|
||||
return E_POINTER;
|
||||
}
|
||||
*actual = 0;
|
||||
|
||||
if (capacity && !values) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
std::uint32_t available = owner->strings.length - index;
|
||||
std::uint32_t taken = capacity < available ? capacity : available;
|
||||
HSTRING* hstrings = reinterpret_cast<HSTRING*>(values);
|
||||
|
||||
for (std::uint32_t i = 0; i < taken; ++i) {
|
||||
HRESULT hr = WindowsDuplicateString(*(HSTRING*) ffListGet(&owner->strings, sizeof(HSTRING), index + i), &hstrings[i]);
|
||||
if (FAILED(hr)) {
|
||||
// Whatever was handed out already belongs to the caller, so a half filled array has to
|
||||
// be taken back before the error goes out: the caller has no way to free a partial one.
|
||||
for (std::uint32_t j = 0; j < i; ++j) {
|
||||
WindowsDeleteString(hstrings[j]);
|
||||
}
|
||||
return hr;
|
||||
}
|
||||
}
|
||||
|
||||
index += taken;
|
||||
*actual = taken;
|
||||
|
||||
return taken == capacity ? S_OK : S_FALSE;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
HRESULT ffWinrtCreateHstringIterable(const wchar_t* const* items, uint32_t count, abi_t<winrt::Windows::Foundation::Collections::IIterable<winrt::hstring>>** result) {
|
||||
*result = nullptr;
|
||||
|
||||
if (count > 0 && !items) {
|
||||
return E_INVALIDARG;
|
||||
}
|
||||
|
||||
auto* iterable = (FFWinrtHstringIterable*) malloc(sizeof(FFWinrtHstringIterable));
|
||||
if (!iterable) {
|
||||
return E_OUTOFMEMORY;
|
||||
}
|
||||
|
||||
::new (iterable) FFWinrtHstringIterable();
|
||||
iterable->refCount = 1;
|
||||
ffListInitA(&iterable->strings, sizeof(HSTRING), count > 0 ? count : FF_LIST_DEFAULT_ALLOC);
|
||||
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
HSTRING* slot = (HSTRING*) ffListAdd(&iterable->strings, sizeof(HSTRING));
|
||||
// ffListAdd does not zero the slot it hands out, and Release() walks every one of them, so
|
||||
// the slot has to hold a usable HSTRING before WindowsCreateString gets a chance to fail.
|
||||
*slot = nullptr;
|
||||
HRESULT hr = WindowsCreateString(items[i], (UINT32) ::wcslen(items[i]), slot);
|
||||
if (FAILED(hr)) {
|
||||
iterable->Release();
|
||||
return hr;
|
||||
}
|
||||
}
|
||||
|
||||
*result = static_cast<WinrtIterableAbi*>(iterable);
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
#endif // FF_HAVE_WINRT
|
||||
@@ -0,0 +1,132 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
extern "C" {
|
||||
#include "common/FFlist.h"
|
||||
#include "common/FFstrbuf.h"
|
||||
#include "common/time.h"
|
||||
#include "common/windows/com.h"
|
||||
#include "common/windows/unicode.h"
|
||||
}
|
||||
|
||||
#if FF_HAVE_WINRT
|
||||
|
||||
#include <roapi.h>
|
||||
#include <winstring.h>
|
||||
#include <asyncinfo.h>
|
||||
|
||||
#include <winrt/base.h>
|
||||
#include <winrt/Windows.Foundation.h>
|
||||
#include <winrt/Windows.Foundation.Collections.h>
|
||||
|
||||
using winrt::impl::abi_t;
|
||||
|
||||
// Frees an HSTRING and clears the pointer, so it can be used with [[gnu::cleanup]].
|
||||
static inline void ffDeleteHstring(HSTRING* value) {
|
||||
if (*value) {
|
||||
WindowsDeleteString(*value);
|
||||
*value = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// Copies an HSTRING into an FFstrbuf, transcoding from UTF-16 to UTF-8.
|
||||
static inline void ffStrbufSetHstring(FFstrbuf* destination, HSTRING value) {
|
||||
uint32_t length = 0;
|
||||
const wchar_t* raw = WindowsGetStringRawBuffer(value, &length);
|
||||
ffStrbufSetNWS(destination, length, raw);
|
||||
}
|
||||
|
||||
// Wraps `items` in an IVector<hstring> and hands out its IIterable<hstring> view, which is the
|
||||
// parameter shape the FindAllAsync overloads taking additional properties expect. The caller owns
|
||||
// the result.
|
||||
HRESULT ffWinrtCreateHstringIterable(const wchar_t* const* items, uint32_t count, abi_t<winrt::Windows::Foundation::Collections::IIterable<winrt::hstring>>** result);
|
||||
|
||||
// RoGetActivationFactory() for a WinRT runtime class.
|
||||
//
|
||||
// `Projection` must be the *projection* type -- e.g.
|
||||
// winrt::Windows::Devices.Bluetooth::IBluetoothLEDeviceStatics -- and never abi_t<...>:
|
||||
// winrt::impl::guid_v is only specialized for projection types, and abi_t<T> expands to
|
||||
// impl::abi<T>::type, which is a non-deduced context.
|
||||
template <typename Projection>
|
||||
static inline HRESULT ffGetActivationFactory(const wchar_t* className, abi_t<Projection>** factory) {
|
||||
HSTRING_HEADER header;
|
||||
HSTRING runtimeClass;
|
||||
HRESULT hr = WindowsCreateStringReference(className, (UINT32) ::wcslen(className), &header, &runtimeClass);
|
||||
if (FAILED(hr)) {
|
||||
return hr;
|
||||
}
|
||||
|
||||
return RoGetActivationFactory(runtimeClass, winrt::guid_of<Projection>(), reinterpret_cast<void**>(factory));
|
||||
}
|
||||
|
||||
template <typename TargetProjection, typename SourceAbi>
|
||||
static inline HRESULT ffQueryInterface(SourceAbi* source, abi_t<TargetProjection>** target) {
|
||||
return source->QueryInterface(winrt::guid_of<TargetProjection>(), reinterpret_cast<void**>(target));
|
||||
}
|
||||
|
||||
// Spins until `operation` leaves AsyncStatus::Started, then hands its results to `result`.
|
||||
template <typename TOperationAbi, typename TResultAbi>
|
||||
static HRESULT ffWaitForAsyncOperation(TOperationAbi* operation, TResultAbi** result) {
|
||||
FF_AUTO_RELEASE_COM_OBJECT IAsyncInfo* asyncInfo = nullptr;
|
||||
HRESULT hr = ffQueryInterface<IAsyncInfo>(operation, &asyncInfo);
|
||||
if (FAILED(hr)) {
|
||||
return hr;
|
||||
}
|
||||
|
||||
AsyncStatus status = AsyncStatus::Started;
|
||||
|
||||
for (;;) {
|
||||
hr = asyncInfo->get_Status(&status);
|
||||
if (FAILED(hr)) {
|
||||
return hr;
|
||||
}
|
||||
if (status == AsyncStatus::Started) {
|
||||
ffTimeSleep(0);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (status != AsyncStatus::Completed) {
|
||||
HRESULT errorCode = E_FAIL;
|
||||
asyncInfo->get_ErrorCode(&errorCode);
|
||||
return FAILED(errorCode) ? errorCode : E_FAIL;
|
||||
}
|
||||
|
||||
return operation->GetResults((void**) result);
|
||||
}
|
||||
|
||||
template <typename TResultProjection, typename TOperation>
|
||||
static HRESULT ffRunAndWait(TOperation&& operation, abi_t<TResultProjection>** result) {
|
||||
*result = nullptr;
|
||||
|
||||
FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Foundation::IAsyncOperation<TResultProjection>>* opResult = nullptr;
|
||||
HRESULT hr = operation(reinterpret_cast<void**>(&opResult));
|
||||
if (FAILED(hr) || !opResult) {
|
||||
return hr;
|
||||
}
|
||||
|
||||
return ffWaitForAsyncOperation(opResult, result);
|
||||
}
|
||||
|
||||
template <typename TResultProjection, typename TOperation>
|
||||
static HRESULT ffRunAndWait2(TOperation&& operation, abi_t<TResultProjection>** result) {
|
||||
*result = nullptr;
|
||||
|
||||
FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Foundation::IAsyncOperationWithProgress<TResultProjection, int32_t>>* opResult = nullptr;
|
||||
HRESULT hr = operation(reinterpret_cast<void**>(&opResult));
|
||||
if (FAILED(hr) || !opResult) {
|
||||
return hr;
|
||||
}
|
||||
|
||||
return ffWaitForAsyncOperation(opResult, result);
|
||||
}
|
||||
|
||||
#endif // FF_HAVE_WINRT
|
||||
|
||||
#else
|
||||
|
||||
#error Must be included in C++ source file
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,111 @@
|
||||
#pragma once
|
||||
|
||||
#include "fastfetch.h"
|
||||
|
||||
#if FF_HAVE_ZLIB
|
||||
|
||||
// Which set of entry points a caller needs. Resolving is per group rather than all at once so that a
|
||||
// symbol nobody uses cannot make a caller fail: libz is not one library across the platforms
|
||||
// fastfetch builds on, and the gz family in particular is not guaranteed to be present.
|
||||
typedef enum FFZlibGroup : uint8_t {
|
||||
FF_ZLIB_COMPRESS = 1 << 0, // compressBound, compress2
|
||||
FF_ZLIB_INFLATE = 1 << 1, // inflateInit2_, inflate, inflateEnd
|
||||
FF_ZLIB_GZFILE = 1 << 2, // gzopen, gzread, gzclose
|
||||
} FFZlibGroup;
|
||||
|
||||
#if FF_DISABLE_DLOPEN
|
||||
|
||||
#include <zlib.h>
|
||||
|
||||
static inline const char* ffZlibLoad([[maybe_unused]] FFZlibGroup group) {
|
||||
// the library is linked in: nothing to open, nothing to resolve, nothing to fail
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
#else
|
||||
// Hack. <zlib.h> declares everything with an extern linkage macro, which prevents us from
|
||||
// implementing the functions ourselves. zlib spells it ZEXTERN/ZEXPORT and zlib-ng's
|
||||
// zlib-compat header spells it Z_EXTERN/Z_EXPORT; both set theirs under `#ifndef`, so
|
||||
// defining all four before the include is what makes the definitions at the bottom of this
|
||||
// header legal -- they inherit the static inline linkage from the declarations zlib.h emits
|
||||
// here. This header must therefore be the first thing in a translation unit to pull in
|
||||
// <zlib.h>; if it is not, the definitions below stop being static and clang reports
|
||||
// `static declaration follows non-static declaration` right there.
|
||||
#undef ZEXTERN
|
||||
#undef ZEXPORT
|
||||
#undef Z_EXTERN
|
||||
#undef Z_EXPORT
|
||||
#define ZEXTERN static inline
|
||||
#define ZEXPORT
|
||||
#define Z_EXTERN static inline
|
||||
#define Z_EXPORT
|
||||
|
||||
// Redeclaring everything as `static inline` above means <zlib.h> now defines entry points
|
||||
// that a given translation unit may not call at all. Silenced for the include only: the
|
||||
// pragma is popped right after, so the rest of the file -- and the rest of the translation
|
||||
// unit that includes this header -- still reports unused functions.
|
||||
|
||||
// Note: `#pragma GCC diagnostic push` is intentionally omitted here.
|
||||
#pragma GCC diagnostic ignored "-Wunused-function"
|
||||
|
||||
#include <zlib.h>
|
||||
|
||||
#include "common/library.h"
|
||||
|
||||
extern struct FFZlibData {
|
||||
// Kept open rather than closed once opened: every group resolves against this one mapping, and
|
||||
// re-opening per group would pay the cold load again.
|
||||
void* handle;
|
||||
|
||||
FF_LIBRARY_SYMBOL(compressBound)
|
||||
FF_LIBRARY_SYMBOL(compress2)
|
||||
FF_LIBRARY_SYMBOL(inflateInit2_)
|
||||
FF_LIBRARY_SYMBOL(inflate)
|
||||
FF_LIBRARY_SYMBOL(inflateEnd)
|
||||
FF_LIBRARY_SYMBOL(gzopen)
|
||||
FF_LIBRARY_SYMBOL(gzread)
|
||||
FF_LIBRARY_SYMBOL(gzclose)
|
||||
|
||||
FFZlibGroup attempted;
|
||||
} zlibData;
|
||||
|
||||
[[gnu::always_inline]] static inline uLong(compressBound)(uLong sourceLen) {
|
||||
return zlibData.ffcompressBound(sourceLen);
|
||||
}
|
||||
|
||||
[[gnu::always_inline]] static inline int(compress2)(Bytef* dest, uLongf* destLen, const Bytef* source, uLong sourceLen, int level) {
|
||||
return zlibData.ffcompress2(dest, destLen, source, sourceLen, level);
|
||||
}
|
||||
|
||||
[[gnu::always_inline]] static inline int(inflateInit2_)(z_streamp strm, int windowBits, const char* version, int stream_size) {
|
||||
return zlibData.ffinflateInit2_(strm, windowBits, version, stream_size);
|
||||
}
|
||||
|
||||
[[gnu::always_inline]] static inline int(inflate)(z_streamp strm, int flush) {
|
||||
return zlibData.ffinflate(strm, flush);
|
||||
}
|
||||
|
||||
[[gnu::always_inline]] static inline int(inflateEnd)(z_streamp strm) {
|
||||
return zlibData.ffinflateEnd(strm);
|
||||
}
|
||||
|
||||
[[gnu::always_inline]] static inline gzFile(gzopen)(const char* path, const char* mode) {
|
||||
return zlibData.ffgzopen(path, mode);
|
||||
}
|
||||
|
||||
[[gnu::always_inline]] static inline int(gzread)(gzFile file, voidp buf, unsigned len) {
|
||||
return zlibData.ffgzread(file, buf, len);
|
||||
}
|
||||
|
||||
[[gnu::always_inline]] static inline int(gzclose)(gzFile file) {
|
||||
return zlibData.ffgzclose(file);
|
||||
}
|
||||
|
||||
// Resolves the entry points of `group` and returns nullptr, or an error message. Call it before using
|
||||
// that group's functions: until it has succeeded, the forwarders above are null pointers. The library
|
||||
// itself is opened once, by whichever group is asked for first, and stays open.
|
||||
const char* ffZlibLoad(FFZlibGroup group);
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -2,35 +2,119 @@
|
||||
#include "battery.h"
|
||||
#include "common/android/api.h"
|
||||
#include "common/android/binder.h"
|
||||
#include "common/android/dex.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/io.h"
|
||||
#include "common/processing.h"
|
||||
#include "common/properties.h"
|
||||
#include "common/settings.h"
|
||||
#include "common/strutil.h"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <inttypes.h>
|
||||
#include <string.h>
|
||||
|
||||
// Android exposes the battery through BatteryManager, which the NDK does not wrap and whose sysfs
|
||||
// backing is unreadable from an app UID. What does work without any permission is
|
||||
// android.os.IBatteryPropertiesRegistrar in system_server, reachable either over /dev/binder (see
|
||||
// common/android/binder.h) or through a `dumpsys battery` subprocess. The binder route is the cheap
|
||||
// one: ~0.3 ms for both properties, and no child process.
|
||||
// backing is unreadable from an app UID -- /sys/class/power_supply is closed to untrusted_app
|
||||
// outright, directory and all. What does work without any permission is
|
||||
// android.os.IBatteryPropertiesRegistrar in system_server, over /dev/binder (see
|
||||
// common/android/binder.h).
|
||||
//
|
||||
// The registrar only answers properties that do not need BATTERY_STATS, and that rules out
|
||||
// everything the module would otherwise like to show. Manufacturer, model name, technology, serial
|
||||
// number, manufacture date, temperature and cycle count all sit behind that permission.
|
||||
// `dumpsys battery` is the other route, tried first for shell and root because it is the richer of
|
||||
// the two: a flat `key: value` list read by name, so it carries what neither the registrar nor a
|
||||
// property has -- the technology and the critical capacity level -- and neither a vendor that prints
|
||||
// extra keys in the middle nor a release that appends a field can break it. The binder reply is
|
||||
// positional and breaks silently instead. What it costs is a fork/exec, which is why it is only
|
||||
// reached where it can succeed; see the UID gate in ffDetectBattery.
|
||||
//
|
||||
// An app UID gets none of that, and not because Android withholds it. `ACTION_BATTERY_CHANGED` is a
|
||||
// sticky broadcast, and a sticky broadcast is read without a permission: it carries the technology,
|
||||
// and a cycle count since Android 14 -- the device's own BatteryManager declares `EXTRA_CYCLE_COUNT`,
|
||||
// spelled "android.os.extra.CYCLE_COUNT". What is out of reach is the call that fetches it. A sticky
|
||||
// broadcast is read by `registerReceiver`, the NDK does not wrap it, and a native binary has no Java
|
||||
// side to make that call from, so every route to it is a Java process. termux-api is one, and it is
|
||||
// not the answer: on the device this was written on it hung on six of six runs, each killed at 30
|
||||
// seconds having written nothing, after exec'ing `am broadcast` and waiting on a socket that has no
|
||||
// timeout of its own.
|
||||
//
|
||||
// What is left within reach is a pair of system properties BatteryService mirrors out of every update
|
||||
// it processes: `debug.tracing.plug_type` and `debug.tracing.battery_status`. A system property is
|
||||
// world readable, which is the whole point of them here -- the charger bits used to need `dumpsys
|
||||
// battery`, and so the shell UID, while the status used to cost a second transaction. Both are now
|
||||
// free, to any UID; see the two defines below.
|
||||
//
|
||||
// The temperature comes from neither service. The kernel publishes it as a thermal zone, and
|
||||
// /sys/class/thermal is readable from an app UID -- unlike /sys/class/power_supply, which is closed
|
||||
// outright, directory and all -- so it is read there whenever `temp` asks for it. The dump has a
|
||||
// `temperature` line of its own and that one wins where the dump runs; see `detectTemperature` below
|
||||
// for why the zone is looked up by name rather than addressed by number.
|
||||
//
|
||||
// The registrar answers nothing else. `BatteryProperty` has no temperature, no technology and no
|
||||
// cycle count, and manufacturer and model name are not battery data at all. Of the properties that
|
||||
// do exist, ids 7 to 12 -- the ones Android 15 added -- are behind BATTERY_STATS, and ids 1 to 6 are
|
||||
// not. The one reading the module wants that is not a property either, how long the charge will
|
||||
// last, comes from a different service; see the `batterystats` block below.
|
||||
|
||||
// The transaction code and the reply layout are both positional, and neither is negotiated with the
|
||||
// service, so each only stays correct as long as AOSP keeps the order it already has. The code has
|
||||
// not: `IBatteryPropertiesRegistrar` still declared registerListener / unregisterListener /
|
||||
// getProperty up to Android 9, and Android 10 dropped the two listener methods, which moved
|
||||
// `getProperty` from the third position to the first. The layout has, because `BatteryProperty` has
|
||||
// only ever grown at the end -- API 35 appended [string8 mValueString] to it -- so the field read
|
||||
// below is still the first one written.
|
||||
// The reply layout is positional and is not negotiated with the service, so it only stays correct as
|
||||
// long as AOSP keeps the order it already has. It has, because `BatteryProperty` has only ever grown
|
||||
// at the end -- API 35 appended [string8 mValueString] to it -- so the field read below is still the
|
||||
// first one written.
|
||||
//
|
||||
// The transaction code is positional for the same reason, and it has *not* stayed: the interface
|
||||
// still declared registerListener / unregisterListener / getProperty up to Android 9, and Android 10
|
||||
// dropped the two listener methods, which moved `getProperty` from the third position to the first.
|
||||
// The number is therefore read out of the device's own jar rather than guessed from the API level,
|
||||
// because a vendor fork is free to insert a method ahead of it -- and a wrong code reaches a
|
||||
// different method, or none. See common/android/dex.h.
|
||||
//
|
||||
// Getting either wrong is quiet rather than loud: `ffBinderReadU64` returns 0 for a read past the
|
||||
// end of the reply, so a drift shows up as a wrong capacity, not as a failed call.
|
||||
#define FF_BATTERY_ANDROID_SERVICE "batteryproperties"
|
||||
#define FF_BATTERY_ANDROID_DESCRIPTOR "android.os.IBatteryPropertiesRegistrar"
|
||||
|
||||
// IBatteryPropertiesRegistrar has always been part of framework.jar -- it is not one of the
|
||||
// frameworks that moved into an APEX -- so there is one path and no fallback. The method this module
|
||||
// calls is named by the constant the dex carries for it, which is what the lookup turns into whatever
|
||||
// number this build uses.
|
||||
#define FF_BATTERY_ANDROID_JAR "/system/framework/framework.jar"
|
||||
#define FF_BATTERY_ANDROID_STUB "Landroid/os/IBatteryPropertiesRegistrar$Stub;"
|
||||
#define FF_BATTERY_ANDROID_GET_PROPERTY "TRANSACTION_getProperty"
|
||||
|
||||
// How long the charge will last is not a property of the pack: it is a forecast BatteryStatsService
|
||||
// makes from the discharge history, so it lives on a second service. That service is `batterystats`,
|
||||
// whose interface is com.android.internal.app.IBatteryStats, and it is also in framework.jar --
|
||||
// com.android.internal.app is not one of the namespaces that moved into an APEX -- so the same read
|
||||
// resolves this code as the one above.
|
||||
//
|
||||
// The interface has 120 methods and the dex cannot say which of them an app UID may call: neither
|
||||
// IBatteryStats nor its $Stub carries an `_enforcePermission` wrapper, so the wall is inside
|
||||
// BatteryStatsService, which lives in services.jar and is not available to read. It was therefore
|
||||
// measured, as uid 10478 on an Android 16 device. The three this module needs answer with no
|
||||
// permission at all -- and so do `getAllWakeLocks` (117) and `getAllWakeLocksEx` (118), which is why
|
||||
// the claim is not "only three of 120". Every read-only statistic getter tried (`getAwakeTimeBattery`,
|
||||
// `getAwakeTimePlugged`, and the cellular / wifi / gps / bluetooth ones) came back with
|
||||
// "Access denied, requires: android.permission.BATTERY_STATS", or an `anyOf` of that and
|
||||
// UPDATE_DEVICE_STATS.
|
||||
//
|
||||
// The code is read out of the jar for the same reason as `getProperty`, and the numbers make the
|
||||
// point better than the argument does: this method is transaction 19 on that Android 16 device, 22
|
||||
// on a Mi 10 (Android 14) and 20 on a Mi 9 (Android 10). A hardcoded 19 would reach a different
|
||||
// method on two of the three.
|
||||
#define FF_BATTERY_ANDROID_STATS_SERVICE "batterystats"
|
||||
#define FF_BATTERY_ANDROID_STATS_DESCRIPTOR "com.android.internal.app.IBatteryStats"
|
||||
#define FF_BATTERY_ANDROID_STATS_STUB "Lcom/android/internal/app/IBatteryStats$Stub;"
|
||||
#define FF_BATTERY_ANDROID_COMPUTE_BATTERY_TIME_REMAINING "TRANSACTION_computeBatteryTimeRemaining"
|
||||
|
||||
// The temperature is a third source again, and not a service this time: the kernel publishes it as a
|
||||
// thermal zone, and the zone Android's own thermal HAL names `battery` is the one to read. The name
|
||||
// is what is matched -- not the number, and not a prefix -- for the reasons spelled out in
|
||||
// detectTemperature below.
|
||||
#define FF_BATTERY_ANDROID_THERMAL_DIR "/sys/class/thermal/"
|
||||
#define FF_BATTERY_ANDROID_THERMAL_ZONE_PREFIX "thermal_zone"
|
||||
#define FF_BATTERY_ANDROID_THERMAL_BATTERY_TYPE "battery"
|
||||
|
||||
// BatteryManager.BATTERY_PROPERTY_*
|
||||
typedef enum FFBatteryAndroidProperty : uint32_t {
|
||||
FF_BATTERY_ANDROID_PROPERTY_CAPACITY = 4,
|
||||
@@ -38,19 +122,49 @@ typedef enum FFBatteryAndroidProperty : uint32_t {
|
||||
} FFBatteryAndroidProperty;
|
||||
|
||||
// BatteryManager.BATTERY_STATUS_*, which the health HAL's `BatteryStatus` mirrors value for value.
|
||||
// Only the three the module has something to say about are carried: NOT_CHARGING (4) and FULL (5)
|
||||
// both mean a charger is attached, which the powered bits already say.
|
||||
// The module has something to say about only three of the five: NOT_CHARGING (4) and FULL (5) both
|
||||
// mean a charger is attached, and it answers nothing about the charger to put that on, while the dump
|
||||
// route maps UNKNOWN (1) as well, because there it is a different statement from the service not
|
||||
// answering at all. The two are named anyway, because the property route validates the whole range
|
||||
// before it trusts a value it did not get from the service.
|
||||
typedef enum FFBatteryAndroidStatus : uint32_t {
|
||||
FF_BATTERY_ANDROID_STATUS_UNKNOWN = 1,
|
||||
FF_BATTERY_ANDROID_STATUS_CHARGING = 2,
|
||||
FF_BATTERY_ANDROID_STATUS_DISCHARGING = 3,
|
||||
FF_BATTERY_ANDROID_STATUS_NOT_CHARGING = 4,
|
||||
FF_BATTERY_ANDROID_STATUS_FULL = 5,
|
||||
} FFBatteryAndroidStatus;
|
||||
|
||||
// The pair of system properties that stand in for two of the readings above. Both are published by
|
||||
// `BatteryService.processValuesLocked` -- the two strings sit next to each other in the constant pool
|
||||
// of classes.dex in this device's services.jar, which is what a pair of `SystemProperties.set` calls
|
||||
// looks like after the fact -- and neither is a `BatteryProperty`, so nothing about the registrar's
|
||||
// positional reply applies to them.
|
||||
//
|
||||
// debug.tracing.plug_type -- BatteryManager.BATTERY_PLUGGED_*: 1 AC, 2 USB, 4 wireless,
|
||||
// 8 dock, and 0 when nothing is attached. The dock bit has no
|
||||
// `FF_BATTERY_STATUS_*` counterpart and is masked off below.
|
||||
// debug.tracing.battery_status -- BatteryManager.BATTERY_STATUS_*, the value
|
||||
// `getProperty(BATTERY_PROPERTY_STATUS)` answers with as well:
|
||||
// 1 UNKNOWN, 2 CHARGING, 3 DISCHARGING, 4 NOT_CHARGING, 5 FULL.
|
||||
#define FF_BATTERY_ANDROID_PROP_PLUG_TYPE "debug.tracing.plug_type"
|
||||
#define FF_BATTERY_ANDROID_PROP_BATTERY_STATUS "debug.tracing.battery_status"
|
||||
|
||||
// The encoding of plug_type, read out of the device's own framework.jar rather than assumed: the
|
||||
// values are bits of one field and not a sequence, so `value != 0` is not a test for "wired", and a
|
||||
// device reporting the dock would be told it is on neither AC nor USB nor wireless.
|
||||
typedef enum FFBatteryAndroidPlugged : uint32_t {
|
||||
FF_BATTERY_ANDROID_PLUGGED_AC = 1,
|
||||
FF_BATTERY_ANDROID_PLUGGED_USB = 2,
|
||||
FF_BATTERY_ANDROID_PLUGGED_WIRELESS = 4,
|
||||
} FFBatteryAndroidPlugged;
|
||||
|
||||
// BatteryCapacityLevel.BATTERY_CAPACITY_LEVEL_CRITICAL, the level the framework itself shuts the
|
||||
// device down on. This is the health HAL's numbering, not the one the older
|
||||
// `BatteryManager.BATTERY_CAPACITY_LEVEL_*` constants used, which had CRITICAL at 4. The rest of the
|
||||
// enum -- -1 UNSUPPORTED, 0 UNKNOWN, 2 LOW, 3 NORMAL, 4 HIGH, 5 FULL -- only restates the
|
||||
// percentage, so only this value is carried.
|
||||
// enum is skipped: -1 (UNSUPPORTED) and 0 (UNKNOWN) are what a HAL that does not implement the
|
||||
// property reports, and 2 to 5 only restate the percentage. Only the dump carries this -- it is not a
|
||||
// `BatteryProperty`, so the binder route never sees it.
|
||||
typedef enum FFBatteryAndroidCapacityLevel : int32_t {
|
||||
FF_BATTERY_ANDROID_CAPACITY_LEVEL_CRITICAL = 1,
|
||||
} FFBatteryAndroidCapacityLevel;
|
||||
@@ -62,6 +176,11 @@ typedef enum FFBatteryAndroidCapacityLevel : int32_t {
|
||||
#define FF_BATTERY_ANDROID_VALUE_OFFSET 12
|
||||
#define FF_BATTERY_ANDROID_VALUE_UNSET 0x8000000000000000ULL // Long.MIN_VALUE, i.e. never filled in
|
||||
|
||||
// A method that answers with a bare `long` writes [int32 exception][int64 value] and nothing else:
|
||||
// there is no out parameter, so no non-null marker sits in front of the value the way it does in a
|
||||
// `getProperty` reply. Twelve bytes in all on the device.
|
||||
#define FF_BATTERY_ANDROID_LONG_VALUE_OFFSET 4
|
||||
|
||||
// One `getProperty` reply is [exception code][return value][value present][int64 mValueLong], and it
|
||||
// measures 24 bytes on the device: API 35 appended [string8 mValueString] to `BatteryProperty`, and a
|
||||
// reply carries a word for it even when it is empty. A dump of the first 32 covers every word.
|
||||
@@ -103,15 +222,8 @@ static void debugDumpReply(const uint8_t* data, size_t size) {
|
||||
#define FF_BATTERY_ANDROID_DEBUG_DUMP(data, size) ((void) 0)
|
||||
#endif
|
||||
|
||||
static const char* getProperty(FFBinder* binder, uint32_t handle, uint32_t property, uint64_t* value) {
|
||||
// `getProperty` is the third method declared in `IBatteryPropertiesRegistrar` up to Android 9 and
|
||||
// the first one from Android 10 on. Written as an `if` because clang rejects `__builtin_available`
|
||||
// -- which is what FF_ANDROID_API_AT_LEAST expands to -- in any other position.
|
||||
uint32_t transaction = 3u;
|
||||
if (FF_ANDROID_API_AT_LEAST(29)) {
|
||||
transaction = 1u;
|
||||
}
|
||||
FF_DEBUG("Property %u goes out as transaction %u (3 up to Android 9, 1 from Android 10)", property, transaction);
|
||||
static const char* getProperty(FFBinder* binder, uint32_t handle, uint32_t transaction, uint32_t property, uint64_t* value) {
|
||||
FF_DEBUG("Property %u goes out as transaction %u", property, transaction);
|
||||
|
||||
uint8_t parcelBuffer[128];
|
||||
FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer));
|
||||
@@ -120,7 +232,7 @@ static const char* getProperty(FFBinder* binder, uint32_t handle, uint32_t prope
|
||||
|
||||
uint8_t replyBuffer[128];
|
||||
FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer));
|
||||
const char* error = ffBinderTransact(binder, handle, transaction, &parcel, &reply);
|
||||
const char* error = ffBinderTransact(binder, handle, transaction, 0, &parcel, &reply);
|
||||
if (error != nullptr) {
|
||||
FF_DEBUG("Property %u could not be transacted: %s", property, error);
|
||||
return error;
|
||||
@@ -164,34 +276,313 @@ static const char* getProperty(FFBinder* binder, uint32_t handle, uint32_t prope
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static const char* parseBinder(FFlist* results) {
|
||||
// Reads either of the two properties above as a number. Both are written by `Integer.toString`, so a
|
||||
// buffer whose text is not a number is the property saying nothing rather than a 0 -- 0 is a plug type,
|
||||
// and it is not a status.
|
||||
static bool readPropertyUInt(const char* name, uint32_t* value) {
|
||||
FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
|
||||
if (!ffSettingsGetAndroidProperty(name, &buffer)) {
|
||||
FF_DEBUG("The \"%s\" property is not set, so nothing is read from it", name);
|
||||
return false;
|
||||
}
|
||||
|
||||
// `ffStrbufToUInt` answers the default for text that does not start with a digit, which is why the
|
||||
// default is a value neither property can hold rather than a 0.
|
||||
const uint64_t parsed = ffStrbufToUInt(&buffer, UINT64_MAX);
|
||||
if (parsed == UINT64_MAX) {
|
||||
FF_DEBUG("The \"%s\" property reads \"%s\", which is not a number", name, buffer.chars);
|
||||
return false;
|
||||
}
|
||||
|
||||
*value = (uint32_t) parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
// The charger type. This is what the property route has and the registrar does not: `getProperty`
|
||||
// answers nothing about the charger, so the three bits used to come from `dumpsys battery`, i.e. from
|
||||
// the shell UID and root alone. A device that does not publish the property leaves the bits clear,
|
||||
// which is also what an unplugged charger looks like -- the module cannot tell those apart, and neither
|
||||
// can the framework, whose sticky broadcast carries this same value as `EXTRA_PLUGGED`.
|
||||
static void fillPlugged(FFBatteryResult* battery) {
|
||||
uint32_t plugged = 0;
|
||||
if (!readPropertyUInt(FF_BATTERY_ANDROID_PROP_PLUG_TYPE, &plugged)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A mask rather than a comparison: the three are bits of one field, and it is the bits that have a
|
||||
// `FF_BATTERY_STATUS_*` counterpart. BATTERY_PLUGGED_DOCK (8) is the one that does not, so a device
|
||||
// on a dock reports no powered bit at all -- and `plugged != 0` would have called that "wired".
|
||||
if (plugged & FF_BATTERY_ANDROID_PLUGGED_AC) {
|
||||
battery->status |= FF_BATTERY_STATUS_AC_CONNECTED;
|
||||
}
|
||||
if (plugged & FF_BATTERY_ANDROID_PLUGGED_USB) {
|
||||
battery->status |= FF_BATTERY_STATUS_USB_CONNECTED;
|
||||
}
|
||||
if (plugged & FF_BATTERY_ANDROID_PLUGGED_WIRELESS) {
|
||||
battery->status |= FF_BATTERY_STATUS_WIRELESS_CONNECTED;
|
||||
}
|
||||
|
||||
FF_DEBUG("The \"%s\" property is %u, which becomes status bits 0x%x (1 AC, 2 USB, 4 wireless; 0 is "
|
||||
"what both an unplugged charger and a device that does not publish the property look like)",
|
||||
FF_BATTERY_ANDROID_PROP_PLUG_TYPE, plugged, (unsigned) battery->status);
|
||||
}
|
||||
|
||||
// The status, read the cheap way first. The property carries the same `mHealthInfo.batteryStatus` the
|
||||
// transaction would fetch, so the two can only differ inside the window between the health HAL reporting
|
||||
// a value and the service storing it, while what the property costs is a read of a shared page instead
|
||||
// of a round trip through /dev/binder -- a round trip measured at about 0.04 ms on the device this was
|
||||
// written on, and what it saves is a positional reply that has to be decoded as well.
|
||||
//
|
||||
// It stays a preference rather than a replacement: a property that is not set (an early boot, or a
|
||||
// build whose BatteryService does not publish it) or that does not carry one of the five values sends
|
||||
// the caller to the service, which is the one answer that cannot be stale.
|
||||
static bool detectStatusFromProperty(uint64_t* status) {
|
||||
uint32_t value = 0;
|
||||
if (!readPropertyUInt(FF_BATTERY_ANDROID_PROP_BATTERY_STATUS, &value)) {
|
||||
return false;
|
||||
}
|
||||
if (value < FF_BATTERY_ANDROID_STATUS_UNKNOWN || value > FF_BATTERY_ANDROID_STATUS_FULL) {
|
||||
FF_DEBUG("The \"%s\" property is %u, which is not a battery status, so the service is asked",
|
||||
FF_BATTERY_ANDROID_PROP_BATTERY_STATUS, value);
|
||||
return false;
|
||||
}
|
||||
|
||||
*status = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Calls a method that takes no argument and answers with one `long`, which is how
|
||||
// IBatteryStats.computeBatteryTimeRemaining() is declared. `transaction` comes out of the jar, and the
|
||||
// sentinel the reader leaves for a code it could not find is caught here, so that a build which does
|
||||
// not declare the method is a message rather than a transaction that means something else.
|
||||
//
|
||||
// A negative answer is not an error: it is how the interface spells "no estimate", so it is passed
|
||||
// back as it came and the caller decides what that is worth. The unit is milliseconds.
|
||||
static const char* getTimeRemaining(FFBinder* binder, uint32_t handle, int32_t transaction, int64_t* milliseconds) {
|
||||
if (transaction == FF_DEX_STATIC_INT_UNRESOLVED) {
|
||||
FF_DEBUG("computeBatteryTimeRemaining is unresolved in the jar (transaction %d)", (int) transaction);
|
||||
return "The battery stats service does not declare computeBatteryTimeRemaining";
|
||||
}
|
||||
|
||||
uint8_t parcelBuffer[128];
|
||||
FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer));
|
||||
ffBinderParcelPutInterfaceToken(&parcel, FF_BATTERY_ANDROID_STATS_DESCRIPTOR);
|
||||
|
||||
uint8_t replyBuffer[128];
|
||||
FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer));
|
||||
const char* error = ffBinderTransact(binder, handle, (uint32_t) transaction, 0, &parcel, &reply);
|
||||
if (error != nullptr) {
|
||||
FF_DEBUG("computeBatteryTimeRemaining could not be transacted: %s", error);
|
||||
return error;
|
||||
}
|
||||
if (ffBinderReplyIsStatus(&reply)) {
|
||||
FF_DEBUG("computeBatteryTimeRemaining came back as a status reply of %d instead of a parcel",
|
||||
(int) ffBinderReadI32(reply.data, reply.size, 0));
|
||||
return "The battery stats service rejected the request";
|
||||
}
|
||||
|
||||
// The exception word is read first and the value only once it is known to be a value: a denied
|
||||
// call answers [exception -1][string16 message], so the eight bytes at the value's offset are
|
||||
// then the length and the first characters of the message rather than a number.
|
||||
const int32_t exception = ffBinderReadI32(reply.data, reply.size, 0);
|
||||
if (exception != 0) {
|
||||
FF_DEBUG("computeBatteryTimeRemaining is transaction %d, reply %zu bytes, exception %d",
|
||||
transaction, reply.size, exception);
|
||||
return "The battery stats service raised an exception";
|
||||
}
|
||||
|
||||
*milliseconds = (int64_t) ffBinderReadU64(reply.data, reply.size, FF_BATTERY_ANDROID_LONG_VALUE_OFFSET);
|
||||
FF_DEBUG("computeBatteryTimeRemaining is transaction %d: reply is %zu bytes, value %" PRId64 " ms",
|
||||
transaction, reply.size, *milliseconds);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Writes the time estimate onto the battery just reported. It is not one of the registrar's
|
||||
// properties -- it is a forecast BatteryStatsService makes -- so it comes from a second service and
|
||||
// is filled in here, after the capacity and the status are already in place.
|
||||
//
|
||||
// `binder` and `handle` are the caller's, already open on `batterystats`. Nothing here can fail the
|
||||
// module: an estimate that is not available is what `FFBatteryResult` already spells with -1.
|
||||
static void fillTimeRemaining(FFBinder* binder, uint32_t handle, int32_t transaction, FFlist* results) {
|
||||
if (results->length == 0) {
|
||||
FF_DEBUG("No time remaining is reported: no battery was reported to put it on");
|
||||
return;
|
||||
}
|
||||
|
||||
int64_t milliseconds = 0;
|
||||
const char* error = getTimeRemaining(binder, handle, transaction, &milliseconds);
|
||||
if (error != nullptr) {
|
||||
FF_DEBUG("The time remaining could not be computed, so none is reported: %s", error);
|
||||
return;
|
||||
}
|
||||
if (milliseconds <= 0) {
|
||||
FF_DEBUG("The battery stats service answered %" PRId64 " ms, which is how it says it has no estimate",
|
||||
milliseconds);
|
||||
return;
|
||||
}
|
||||
|
||||
// The interface answers in milliseconds and `FFBatteryResult` carries seconds, which is what the
|
||||
// other platform modules fill in. An answer under a second truncates to 0, which the module reads
|
||||
// as "unknown" as well, so there is nothing to guard against.
|
||||
FF_LIST_GET(FFBatteryResult, *results, 0)->timeRemaining = (int32_t) (milliseconds / 1000);
|
||||
}
|
||||
|
||||
// The battery temperature is neither a registrar property nor part of the health HAL: the kernel
|
||||
// publishes it as a thermal zone, and the zone Android's thermal HAL names `battery` is the one to
|
||||
// read. It is read from /sys/class/thermal, which an app UID may open -- the reason the module does
|
||||
// not simply read /sys/class/power_supply/battery/temp is that that directory is closed to it.
|
||||
//
|
||||
// The zone is *found*, not addressed. Its number is a property of the probe order rather than of the
|
||||
// hardware -- the battery is `thermal_zone67` on the device this was written on, and nothing promises
|
||||
// the next one -- so the directory is walked and each entry is matched on the `type` it reports.
|
||||
//
|
||||
// That match is an exact string, and has to stay one. A prefix would be cheaper and wrong: the same
|
||||
// directory holds hardware trip points (`cpu-hw-trip-0` reports 95000, a perfectly believable 95
|
||||
// degrees), current and battery levels (`pmih010x-ibat-lvl0`, 0 or a small integer) and raw registers
|
||||
// (`vbat`, in millivolts). None of those are a temperature, and every one of them would be printed as
|
||||
// one. Reading a different zone is not a smaller error than reading none.
|
||||
//
|
||||
// A device that names its battery zone something else therefore reports no temperature rather than a
|
||||
// wrong one, which is what FF_BATTERY_TEMP_UNSET already means.
|
||||
static double detectTemperature(void) {
|
||||
FF_AUTO_CLOSE_DIR DIR* dirp = opendir(FF_BATTERY_ANDROID_THERMAL_DIR);
|
||||
if (dirp == nullptr) {
|
||||
FF_DEBUG("Opening \"%s\" failed, so no temperature is reported: %s", FF_BATTERY_ANDROID_THERMAL_DIR, strerror(errno));
|
||||
return FF_BATTERY_TEMP_UNSET;
|
||||
}
|
||||
|
||||
FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
|
||||
struct dirent* entry;
|
||||
while ((entry = readdir(dirp)) != nullptr) {
|
||||
if (!ffStrStartsWith(entry->d_name, FF_BATTERY_ANDROID_THERMAL_ZONE_PREFIX)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
FF_AUTO_CLOSE_FD int zone = openat(dirfd(dirp), entry->d_name, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
||||
if (zone < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The type file ends with a newline, which the comparison below would otherwise never survive.
|
||||
if (!ffReadFileBufferRelative(zone, "type", &buffer)) {
|
||||
continue;
|
||||
}
|
||||
ffStrbufTrimRightSpace(&buffer);
|
||||
if (!ffStrbufEqualS(&buffer, FF_BATTERY_ANDROID_THERMAL_BATTERY_TYPE)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ffReadFileBufferRelative(zone, "temp", &buffer)) {
|
||||
FF_DEBUG("The battery zone \"%s\" has no readable temperature", entry->d_name);
|
||||
return FF_BATTERY_TEMP_UNSET;
|
||||
}
|
||||
|
||||
// sysfs thermal publishes millidegrees Celsius. That is the one thing every zone here agrees
|
||||
// on, even the ones whose reading is not a temperature at all.
|
||||
const double value = ffStrbufToDouble(&buffer, FF_BATTERY_TEMP_UNSET);
|
||||
if (value == FF_BATTERY_TEMP_UNSET) {
|
||||
FF_DEBUG("The battery zone \"%s\" reports \"%s\", which is not a number", entry->d_name, buffer.chars);
|
||||
return FF_BATTERY_TEMP_UNSET;
|
||||
}
|
||||
|
||||
FF_DEBUG("The battery zone is \"%s\", reporting %.2f degrees", entry->d_name, value / 1000.);
|
||||
return value / 1000.;
|
||||
}
|
||||
|
||||
FF_DEBUG("No zone in \"%s\" is named \"%s\", so no temperature is reported",
|
||||
FF_BATTERY_ANDROID_THERMAL_DIR, FF_BATTERY_ANDROID_THERMAL_BATTERY_TYPE);
|
||||
return FF_BATTERY_TEMP_UNSET;
|
||||
}
|
||||
|
||||
// Temperature is opt-in on every platform, and on Android that is not only about the display: the
|
||||
// walk above touches a directory with a hundred-odd entries on the device this was written on, and
|
||||
// none of that is paid for unless `temp` asks for it.
|
||||
static void fillTemperature(FFBatteryOptions* options, FFlist* results) {
|
||||
if (!options->temp) {
|
||||
FF_DEBUG("Temperature is not enabled, so the thermal zones are not walked");
|
||||
return;
|
||||
}
|
||||
if (results->length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const double temperature = detectTemperature();
|
||||
if (temperature != FF_BATTERY_TEMP_UNSET) {
|
||||
FF_LIST_GET(FFBatteryResult, *results, 0)->temperature = temperature;
|
||||
}
|
||||
}
|
||||
|
||||
const char* ffDetectBattery(FFBatteryOptions* options, FFlist* results) {
|
||||
// Resolved before the binder is opened, so that a jar which does not declare the method says so
|
||||
// rather than the module failing later with a message about the transport. Both codes are asked
|
||||
// for in one call, which is cheaper than two: the reader walks the jar's dex entries in order and
|
||||
// stops once every request has been answered, so a second request only costs the walk to the
|
||||
// entry that defines the second class. That walk is not free here, because the two classes are
|
||||
// not in the same entry -- `IBatteryPropertiesRegistrar$Stub` is in classes3.dex and
|
||||
// `IBatteryStats$Stub` in classes5.dex -- and on the device this was measured on it added 1.3 ms
|
||||
// to a module that had been 3.3 ms, while the service lookup and the transaction behind it stayed
|
||||
// inside the noise. The reader reports a code it cannot find by leaving the sentinel in the
|
||||
// result and says nothing more -- what a missing code means for this module is decided here, not
|
||||
// there.
|
||||
//
|
||||
// The two are not equally important. `getProperty` is what the module is made of, so a build
|
||||
// without it has nothing to report; `computeBatteryTimeRemaining` only adds the time estimate,
|
||||
// and a build without it still has the capacity and the status, so its sentinel is carried on to
|
||||
// where the estimate is asked for instead of failing the route here.
|
||||
int32_t transaction = FF_DEX_STATIC_INT_UNRESOLVED;
|
||||
int32_t timeRemainingTransaction = FF_DEX_STATIC_INT_UNRESOLVED;
|
||||
const FFDexStaticIntRequest requests[] = {
|
||||
{ FF_BATTERY_ANDROID_STUB, FF_BATTERY_ANDROID_GET_PROPERTY, &transaction },
|
||||
{ FF_BATTERY_ANDROID_STATS_STUB, FF_BATTERY_ANDROID_COMPUTE_BATTERY_TIME_REMAINING, &timeRemainingTransaction },
|
||||
};
|
||||
const char* error = ffDexStaticInts(FF_BATTERY_ANDROID_JAR, requests, ARRAY_SIZE(requests));
|
||||
if (error != nullptr) {
|
||||
FF_DEBUG("Reading the transaction codes from \"%s\" failed: %s", FF_BATTERY_ANDROID_JAR, error);
|
||||
return error;
|
||||
}
|
||||
if (transaction == FF_DEX_STATIC_INT_UNRESOLVED) {
|
||||
FF_DEBUG("getProperty is unresolved in the jar (transaction %d)", (int) transaction);
|
||||
return "The battery service does not declare getProperty";
|
||||
}
|
||||
|
||||
[[gnu::cleanup(ffBinderClose)]] FFBinder binder = { .fd = -1 };
|
||||
const char* error = ffBinderOpen(&binder);
|
||||
error = ffBinderOpen(&binder);
|
||||
if (error != nullptr) {
|
||||
FF_DEBUG("Opening /dev/binder failed: %s", error);
|
||||
return error;
|
||||
}
|
||||
|
||||
uint32_t handle = 0;
|
||||
error = ffBinderLookupService(&binder, FF_BATTERY_ANDROID_SERVICE, FF_BINDER_SM_GET_SERVICE, &handle);
|
||||
// Released on every path out of this function, including the ones below that return early.
|
||||
[[gnu::cleanup(ffBinderServiceHandleRelease)]] FFBinderServiceHandle service = { .binder = &binder };
|
||||
error = ffBinderLookupService(&binder, FF_BATTERY_ANDROID_SERVICE, FF_BINDER_SM_GET_SERVICE, &service.handle);
|
||||
if (error != nullptr) {
|
||||
FF_DEBUG("Looking up the \"%s\" service failed: %s", FF_BATTERY_ANDROID_SERVICE, error);
|
||||
return error;
|
||||
}
|
||||
FF_DEBUG("The \"%s\" service is handle %u", FF_BATTERY_ANDROID_SERVICE, handle);
|
||||
FF_DEBUG("The \"%s\" service is handle %u, getProperty is transaction %d", FF_BATTERY_ANDROID_SERVICE, service.handle, transaction);
|
||||
|
||||
uint64_t capacity = 0;
|
||||
error = getProperty(&binder, handle, FF_BATTERY_ANDROID_PROPERTY_CAPACITY, &capacity);
|
||||
error = getProperty(&binder, service.handle, (uint32_t) transaction, FF_BATTERY_ANDROID_PROPERTY_CAPACITY, &capacity);
|
||||
if (error != nullptr) {
|
||||
FF_DEBUG("The capacity is what failed, so no battery is reported at all");
|
||||
return error;
|
||||
}
|
||||
|
||||
// The status is asked of the property first, because that is where the second transaction on the
|
||||
// service is saved. The service is still asked when the property has nothing to say, and a failure
|
||||
// there is still fatal: the capacity and the status are what this route is made of, and a battery
|
||||
// whose status is unknown is not a battery this route can describe.
|
||||
uint64_t status = 0;
|
||||
error = getProperty(&binder, handle, FF_BATTERY_ANDROID_PROPERTY_STATUS, &status);
|
||||
if (error != nullptr) {
|
||||
FF_DEBUG("The status is what failed, so no battery is reported at all");
|
||||
return error;
|
||||
if (detectStatusFromProperty(&status)) {
|
||||
FF_DEBUG("The status came from the \"%s\" property and is %" PRIu64 ", so the service is not asked "
|
||||
"for it",
|
||||
FF_BATTERY_ANDROID_PROP_BATTERY_STATUS, status);
|
||||
} else {
|
||||
error = getProperty(&binder, service.handle, (uint32_t) transaction, FF_BATTERY_ANDROID_PROPERTY_STATUS, &status);
|
||||
if (error != nullptr) {
|
||||
FF_DEBUG("The status is what failed, so no battery is reported at all");
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
FFBatteryResult* battery = FF_LIST_ADD(FFBatteryResult, *results);
|
||||
@@ -202,187 +593,38 @@ static const char* parseBinder(FFlist* results) {
|
||||
} else if (status == FF_BATTERY_ANDROID_STATUS_DISCHARGING) {
|
||||
battery->status |= FF_BATTERY_STATUS_DISCHARGING;
|
||||
}
|
||||
// The charger type is the one reading this route has that the registrar does not, and it is a
|
||||
// property as well: the three bits used to come from `dumpsys battery`, and so from the shell UID
|
||||
// and root alone.
|
||||
fillPlugged(battery);
|
||||
// The temperature comes from outside this route entirely, so it is asked for here rather than
|
||||
// inside it: nothing about the binder session above has any bearing on it, and a device without a
|
||||
// zone by that name simply leaves the field at the sentinel.
|
||||
fillTemperature(options, results);
|
||||
// The time estimate is the one part of this route that is allowed to be missing: a jar without
|
||||
// the code, a service that is not running and a service that answers -1 all leave the field at
|
||||
// the -1 the module starts it with, which is how `FFBatteryResult` spells "unknown". It is asked
|
||||
// for last for that reason -- a failure here cannot cost the capacity and the status that have
|
||||
// already been read.
|
||||
[[gnu::cleanup(ffBinderServiceHandleRelease)]] FFBinderServiceHandle stats = { .binder = &binder };
|
||||
error = ffBinderLookupService(&binder, FF_BATTERY_ANDROID_STATS_SERVICE, FF_BINDER_SM_GET_SERVICE, &stats.handle);
|
||||
if (error != nullptr) {
|
||||
FF_DEBUG("Looking up the \"%s\" service failed, so no time remaining is reported: %s",
|
||||
FF_BATTERY_ANDROID_STATS_SERVICE, error);
|
||||
} else {
|
||||
fillTimeRemaining(&binder, stats.handle, timeRemainingTransaction, results);
|
||||
}
|
||||
|
||||
// `BatteryStatus` has five values and only two of them say something the powered bits do not, so
|
||||
// the trace spells the mapping out: anything that is not 2 or 3 leaves the bits clear, which is
|
||||
// what UNKNOWN (1), NOT_CHARGING (4) and FULL (5) all come to.
|
||||
// the trace spells the mapping out: anything that is not 2 or 3 leaves the charging / discharging
|
||||
// bits clear, which is what UNKNOWN (1), NOT_CHARGING (4) and FULL (5) all come to -- that a
|
||||
// charger is attached is stated by the bits the plug_type property fills in, not by this one. The
|
||||
// temperature is not repeated here: it was traced where it was read, and it is the field of the
|
||||
// three that can be missing without anything being wrong.
|
||||
FF_DEBUG("Reporting one battery: capacity %.0f%%, status %" PRIu64 " -> bits 0x%x "
|
||||
"(2 charging, 3 discharging; 1/4/5 leave them clear). The registrar answers no "
|
||||
"temperature and no technology, so both stay unset",
|
||||
battery->capacity, status, (unsigned) battery->status);
|
||||
"(2 charging, 3 discharging; 1/4/5 leave them clear), %d s remaining (-1 when there is no "
|
||||
"estimate). The technology stays unset: it rides the sticky broadcast, which this route "
|
||||
"does not read",
|
||||
battery->capacity, status, (unsigned) battery->status, battery->timeRemaining);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// `dumpsys battery` prints what `BatteryService.dumpInternal()` prints: the whole of the health HAL's
|
||||
// `HealthInfo`, plus the timestamps the service keeps for its own broadcast rate limiter. It is a flat
|
||||
// `key: value` list read by name, which is what makes it the sturdier of the two routes -- a vendor
|
||||
// that prints extra keys (some vendors do: `engine`, `soc decimal`, `adapter power`, `board temp status`,
|
||||
// `low bat status`, `reverse wl chg status`, `reverse wl chg exception`, `chg shut vbat`,
|
||||
// `last mode flag` and `last mode keep time`, in the middle of the list) only adds keys nobody asks
|
||||
// for, and a release that appends a field costs nothing. The binder reply is positional and breaks
|
||||
// silently instead.
|
||||
//
|
||||
// What each key carries, per `android.hardware.health.HealthInfo`:
|
||||
//
|
||||
// AC / USB / Wireless / Dock powered -- `charger*Online`, the `plugged` bits of the
|
||||
// ACTION_BATTERY_CHANGED broadcast. Only the first three have a `FF_BATTERY_STATUS_*`
|
||||
// counterpart; `Dock powered` (`BATTERY_PLUGGED_DOCK`) has none and is not read.
|
||||
// Max charging current / Max charging voltage -- `maxChargingCurrentMicroamps` (µA) and
|
||||
// `maxChargingVoltageMicrovolts` (µV): what the charger offers, not what the battery is drawing.
|
||||
// Charge counter -- `batteryChargeCounterUah` (µAh), what is left in the pack. The unit is not
|
||||
// honoured everywhere: MIUI reports the same number as its own dump and three orders of
|
||||
// magnitude below what the registrar answers for the same battery.
|
||||
// status -- `batteryStatus`, i.e. `BatteryStatus`: 1 UNKNOWN, 2 CHARGING, 3 DISCHARGING,
|
||||
// 4 NOT_CHARGING (a charger is attached and the battery is deliberately not taking current),
|
||||
// 5 FULL.
|
||||
// health -- `batteryHealth`, i.e. `BatteryHealth`: 1 UNKNOWN, 2 GOOD, 3 OVERHEAT, 4 DEAD,
|
||||
// 5 OVER_VOLTAGE, 6 UNSPECIFIED_FAILURE, 7 COLD, and, in newer releases, 8 FAIR, 11 NOT_AVAILABLE
|
||||
// and 12 INCONSISTENT.
|
||||
// present -- `batteryPresent`, whether the pack is in the device at all.
|
||||
// level / scale -- `batteryLevel`, the remaining capacity in percent, and `BATTERY_SCALE`, the value
|
||||
// 100% is expressed in. The scale has been 100 on every release, but it is printed rather than
|
||||
// assumed, so it is read.
|
||||
// voltage -- `batteryVoltageMillivolts`. The unit in the name is the recent one: the HAL 1.0 field
|
||||
// was `batteryVoltage` in microvolts, and the AIDL notes that implementations had always filled it
|
||||
// in millivolts, so the rename only wrote down the de-facto unit.
|
||||
// temperature -- `batteryTemperatureTenthsCelsius`, tenths of a degree Celsius.
|
||||
// technology -- `batteryTechnology`, e.g. "Li-poly". The registrar answers no such property, so
|
||||
// together with the temperature this is what the route is here for.
|
||||
// Charging state -- `chargingState`, i.e. `BatteryChargingState`: 0 INVALID, 1 NORMAL, 2 TOO_COLD,
|
||||
// 3 TOO_HOT, 4 LONG_LIFE, 5 ADAPTIVE.
|
||||
// Charging policy -- `chargingPolicy`, i.e. `BatteryChargingPolicy`: 0 INVALID, 1 DEFAULT,
|
||||
// 2 LONG_LIFE, 3 ADAPTIVE. 0 in either means the HAL did not report it.
|
||||
// Capacity level -- `batteryCapacityLevel`, i.e. `BatteryCapacityLevel`: -1 UNSUPPORTED,
|
||||
// 0 UNKNOWN, 1 CRITICAL, 2 LOW, 3 NORMAL, 4 HIGH, 5 FULL.
|
||||
//
|
||||
// Three keys are not battery readings at all and are skipped: the two `Time when the latest updated
|
||||
// value of the ... was sent via battery changed broadcast` lines and `The last voltage value sent via
|
||||
// the battery changed broadcast` are what `BatteryService` keeps to rate limit the broadcast.
|
||||
//
|
||||
// `FFBatteryResult` has a place for the status enum, the level pair, the temperature and the
|
||||
// technology, and for the three powered booleans and the capacity level in the form of status bits.
|
||||
// `voltage`, `health`, `Charge counter` and the two maximums are read by nothing -- there is no field
|
||||
// to put them in -- and are described above so that the next reader does not have to go back to the
|
||||
// service to find out what they are.
|
||||
static const char* parseDumpsys(FFBatteryOptions* options, FFlist* results) {
|
||||
FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
|
||||
if (ffProcessAppendStdOut(&buf, (char*[]) {
|
||||
"/system/bin/dumpsys",
|
||||
"battery",
|
||||
nullptr,
|
||||
}) != nullptr ||
|
||||
buf.length == 0) {
|
||||
return "Executing `/system/bin/dumpsys battery` failed";
|
||||
}
|
||||
|
||||
if (!ffStrbufStartsWithS(&buf, "Current Battery Service state:\n")) {
|
||||
// A refusal, which ffDetectBattery has already ruled out by looking at the UID: an app UID gets
|
||||
// `Can't find service: battery` on Android 16, and the permission denial some other
|
||||
// release would print lands here too. Both exit 0, so this is the only place that notices.
|
||||
return "Invalid `/system/bin/dumpsys battery` result";
|
||||
}
|
||||
|
||||
const char* start = buf.chars + strlen("Current Battery Service state:\n");
|
||||
|
||||
FF_STRBUF_AUTO_DESTROY temp = ffStrbufCreate();
|
||||
if (!ffParsePropLines(start, "present: ", &temp) || !ffStrbufEqualS(&temp, "true")) {
|
||||
return nullptr;
|
||||
}
|
||||
ffStrbufClear(&temp);
|
||||
|
||||
FFBatteryResult* battery = FF_LIST_ADD(FFBatteryResult, *results);
|
||||
initResult(battery);
|
||||
|
||||
if (ffParsePropLines(start, "AC powered: ", &temp) && ffStrbufEqualS(&temp, "true")) {
|
||||
battery->status |= FF_BATTERY_STATUS_AC_CONNECTED;
|
||||
}
|
||||
ffStrbufClear(&temp);
|
||||
|
||||
if (ffParsePropLines(start, "USB powered: ", &temp) && ffStrbufEqualS(&temp, "true")) {
|
||||
battery->status |= FF_BATTERY_STATUS_USB_CONNECTED;
|
||||
}
|
||||
ffStrbufClear(&temp);
|
||||
|
||||
if (ffParsePropLines(start, "Wireless powered: ", &temp) && ffStrbufEqualS(&temp, "true")) {
|
||||
battery->status |= FF_BATTERY_STATUS_WIRELESS_CONNECTED;
|
||||
}
|
||||
ffStrbufClear(&temp);
|
||||
|
||||
// NOT_CHARGING and FULL both mean a charger is attached, which the powered bits already say, and
|
||||
// neither is charging or discharging in the sense `FF_BATTERY_STATUS` means. UNKNOWN is mapped
|
||||
// because it is a different statement from the service not answering at all.
|
||||
if (ffParsePropLines(start, "status: ", &temp)) {
|
||||
switch (ffStrbufToUInt(&temp, 0)) {
|
||||
case FF_BATTERY_ANDROID_STATUS_CHARGING:
|
||||
battery->status |= FF_BATTERY_STATUS_CHARGING;
|
||||
break;
|
||||
case FF_BATTERY_ANDROID_STATUS_DISCHARGING:
|
||||
battery->status |= FF_BATTERY_STATUS_DISCHARGING;
|
||||
break;
|
||||
case FF_BATTERY_ANDROID_STATUS_UNKNOWN:
|
||||
battery->status |= FF_BATTERY_STATUS_UNKNOWN;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
ffStrbufClear(&temp);
|
||||
|
||||
// CRITICAL is the one capacity level that says something the percentage does not. -1 (UNSUPPORTED)
|
||||
// and 0 (UNKNOWN) are what a HAL that does not implement the property reports, and 2 to 5 only
|
||||
// restate the level.
|
||||
if (ffParsePropLines(start, "Capacity level: ", &temp) &&
|
||||
ffStrbufToSInt(&temp, -1) == FF_BATTERY_ANDROID_CAPACITY_LEVEL_CRITICAL) {
|
||||
battery->status |= FF_BATTERY_STATUS_CRITICAL;
|
||||
}
|
||||
ffStrbufClear(&temp);
|
||||
|
||||
{
|
||||
double level = 0, scale = 0;
|
||||
if (ffParsePropLines(start, "level: ", &temp)) {
|
||||
level = ffStrbufToDouble(&temp, -DBL_MAX);
|
||||
}
|
||||
ffStrbufClear(&temp);
|
||||
|
||||
if (ffParsePropLines(start, "scale: ", &temp)) {
|
||||
scale = ffStrbufToDouble(&temp, -DBL_MAX);
|
||||
}
|
||||
ffStrbufClear(&temp);
|
||||
|
||||
if (level > 0 && scale > 0) {
|
||||
battery->capacity = level * 100 / scale;
|
||||
}
|
||||
}
|
||||
|
||||
if (options->temp) {
|
||||
if (ffParsePropLines(start, "temperature: ", &temp)) {
|
||||
battery->temperature = ffStrbufToDouble(&temp, FF_BATTERY_TEMP_UNSET);
|
||||
if (battery->temperature != FF_BATTERY_TEMP_UNSET) {
|
||||
battery->temperature /= 10.0; // Android returns temperature in tenths of a degree
|
||||
}
|
||||
}
|
||||
ffStrbufClear(&temp);
|
||||
}
|
||||
|
||||
ffParsePropLines(start, "technology: ", &battery->technology);
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char* ffDetectBattery(FFBatteryOptions* options, FFlist* results) {
|
||||
// `dumpsys battery` needs android.permission.DUMP, which only the shell UID and root hold: an app
|
||||
// UID is answered with `Can't find service: battery` on stdout and a zero exit status, so forking
|
||||
// it there costs a child process and cannot succeed. It is also the richer of the two routes --
|
||||
// the registrar answers neither temperature nor technology, and the dump is what spells out the
|
||||
// status enum -- so it is tried first wherever it is allowed to run, and the binder route covers
|
||||
// everything else at ~0.3 ms. termux-api was dropped: it returns nothing on this device and can
|
||||
// hang for minutes, the same reason its camera path was removed.
|
||||
if (ffAndroidIsRootOrShell(instance.state.platform.uid)) {
|
||||
const char* error = parseDumpsys(options, results);
|
||||
if (error == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
FF_DEBUG("`/system/bin/dumpsys battery` failed: %s. Using the binder route instead", error);
|
||||
}
|
||||
|
||||
return parseBinder(results);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user