mirror of
https://github.com/drakmor/ShadowMountPlus.git
synced 2026-10-19 21:29:58 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24fba2be21 | ||
|
|
f20b97db65 | ||
|
|
9d6ab61369 | ||
|
|
25182da557 | ||
|
|
72cf4460a9 | ||
|
|
cc0f4e0684 | ||
|
|
8cdcd112f5 | ||
|
|
c41c3eeba8 | ||
|
|
3d6a300742 | ||
|
|
51d01011ff | ||
|
|
9e803cff7f | ||
|
|
dee175e2bd | ||
|
|
860e9de3b1 | ||
|
|
e0b41b3ca6 | ||
|
|
5c4470cdcd | ||
|
|
5b6be76272 | ||
|
|
c66bff6d2a | ||
|
|
2df61d7e2d | ||
|
|
e960fc8079 | ||
|
|
d1c6978e78 | ||
|
|
4eadb06f57 | ||
|
|
78f536b41a | ||
|
|
99e000eed9 | ||
|
|
8622d8757f | ||
|
|
bcffa18997 | ||
|
|
e00dda0075 | ||
|
|
315265f5db | ||
|
|
30b04358f4 | ||
|
|
a6ad4922a9 | ||
|
|
ebd649e0ea | ||
|
|
4072ced15d | ||
|
|
fee1029b0a | ||
|
|
cbd0fce4f9 | ||
|
|
9fa6ad94de | ||
|
|
d4c4cda739 | ||
|
|
954b3a7d17 | ||
|
|
3652f506c0 | ||
|
|
6c188a03bb | ||
|
|
2680ed28d7 | ||
|
|
f401660113 | ||
|
|
11a135cf1c | ||
|
|
146942c138 | ||
|
|
a8f1a216bd | ||
|
|
c37088543d | ||
|
|
059f8b40f1 | ||
|
|
6f61e8d29e | ||
|
|
a5471da456 | ||
|
|
0abfecd3e7 | ||
|
|
9f1786905c | ||
|
|
fa714f868f | ||
|
|
17bb561e8d | ||
|
|
0efeec16fa | ||
|
|
a584121d76 | ||
|
|
45da514549 | ||
|
|
e272c9736c | ||
|
|
902a6917df | ||
|
|
6df8a472e3 | ||
|
|
4e9afa7885 | ||
|
|
40967215b4 | ||
|
|
a23822a26f | ||
|
|
97ffedf0df | ||
|
|
bdd3083d65 | ||
|
|
bcb906e5b6 | ||
|
|
47a033b50b | ||
|
|
47bded61da | ||
|
|
5d0ca6d5d1 | ||
|
|
ebfe0376c6 | ||
|
|
a6a5437a42 | ||
|
|
8c1ebbdf28 | ||
|
|
853ff28b85 | ||
|
|
e9098631c1 | ||
|
|
bf24df419b | ||
|
|
ff70cb1bc8 | ||
|
|
9254525a6c | ||
|
|
26ba93c9aa | ||
|
|
43d61dce37 | ||
|
|
7759b83293 | ||
|
|
0eca3fa138 | ||
|
|
e30a296a53 | ||
|
|
b374c3163e | ||
|
|
a71a03d0db | ||
|
|
56e8ba2185 | ||
|
|
18a97ecca9 | ||
|
|
270264cc75 | ||
|
|
ef814ad56b | ||
|
|
cec94656c6 | ||
|
|
4ff9a3dffe | ||
|
|
fe7dad099c | ||
|
|
c02f237ddf | ||
|
|
d318b5e607 | ||
|
|
6f97cde602 | ||
|
|
edcac6bdd7 | ||
|
|
27feba2085 | ||
|
|
82603fce7d | ||
|
|
01b64346c8 | ||
|
|
7e48103149 | ||
|
|
124e757199 | ||
|
|
0fcc970a20 | ||
|
|
7684e8431d | ||
|
|
3166434db1 | ||
|
|
c785daaddf | ||
|
|
2db6b49439 | ||
|
|
aff2fc0e6c | ||
|
|
ae36dd5871 | ||
|
|
cc3e07d201 | ||
|
|
2c03ede25c | ||
|
|
a212a883b3 | ||
|
|
99a2956c28 | ||
|
|
93071924b5 | ||
|
|
47e47477d1 | ||
|
|
31c28e1896 | ||
|
|
c1f34ffa22 | ||
|
|
851c269647 | ||
|
|
c9b3415048 | ||
|
|
11b32a97cf | ||
|
|
a43401391d | ||
|
|
0f7bc24928 | ||
|
|
1db534cf82 | ||
|
|
3f81ae589f | ||
|
|
32ae0c9c50 | ||
|
|
5564b4396d | ||
|
|
abe6bbb92b | ||
|
|
896e5af0fd | ||
|
|
15776edb8b | ||
|
|
1bc6477ea7 | ||
|
|
eb2a4c1036 | ||
|
|
8f34cabb20 | ||
|
|
ce74866768 | ||
|
|
f5421c9c5f | ||
|
|
1260396ffd | ||
|
|
7643b3f7f7 | ||
|
|
c00b162a01 | ||
|
|
711e771334 | ||
|
|
2002b0b5b1 | ||
|
|
5d41598348 | ||
|
|
d45db45e45 | ||
|
|
c0f9574dbc | ||
|
|
6393b2f7e0 | ||
|
|
7db8e72604 | ||
|
|
4e2153e349 | ||
|
|
af64dd1967 | ||
|
|
264c555516 | ||
|
|
f23ab55289 | ||
|
|
8e17da3be0 |
No files matched your search
@@ -0,0 +1,2 @@
|
||||
custom:
|
||||
- https://github.com/drakmor/shadowMountPlus#support-development
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
name: Bug Report
|
||||
about: Create a report to help us improve ShadowMountPlus
|
||||
title: "[BUG] Short description of the issue"
|
||||
labels: bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Description**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
Please answer the following questions to help us diagnose the issue:
|
||||
|
||||
**Software Version**
|
||||
- **ShadowMountPlus Version:**
|
||||
|
||||
- **Path to the game that isn't working:**
|
||||
- **Image Type:**
|
||||
- [ ] .fpkg
|
||||
- [ ] .exfat image
|
||||
|
||||
- **How was this image created?**
|
||||
- **Does it work with any other software?**
|
||||
- **Firmware Version & Backports:**
|
||||
**Itemzflow History**
|
||||
- **Has Itemzflow been used for these games before?**
|
||||
- [ ] Yes
|
||||
- [ ] No
|
||||
|
||||
- **If YES: Did you delete the game shortcuts using Itemzflow BEFORE installing ShadowMountPlus?**
|
||||
- [ ] Yes, I deleted them
|
||||
- [ ] No, I did not delete them
|
||||
- [ ] N/A
|
||||
|
||||
**Debug Log**
|
||||
Please attach the `debug.log` file. You can drag and drop the file here, or copy and paste the content inside the spoiler block below.
|
||||
|
||||
<details>
|
||||
<summary>Click to expand debug.log content</summary>
|
||||
|
||||
```text
|
||||
PASTE YOUR LOG HERE
|
||||
```
|
||||
</details>
|
||||
@@ -0,0 +1,12 @@
|
||||
# Files included in the release archive.
|
||||
# One path per line, relative to the repository root.
|
||||
|
||||
shadowmountplus.elf
|
||||
config.ini.example
|
||||
LICENSE
|
||||
make_image.bat
|
||||
mkexfat.sh
|
||||
mkexfat_macos.sh
|
||||
mkufs2.sh
|
||||
New-OsfExfatImage.ps1
|
||||
README.md
|
||||
@@ -0,0 +1,130 @@
|
||||
name: PS5 Build and Release
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- "**/*.md"
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- "**/*.md"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
outputs:
|
||||
archive_name: ${{ steps.package.outputs.archive_name }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Payload
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Checkout pacbrew-repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ps5-payload-dev/pacbrew-repo
|
||||
path: pacbrew-repo
|
||||
|
||||
- name: Checkout SDK stubs
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ps5-payload-dev/sdk
|
||||
path: ps5-sdk
|
||||
|
||||
- name: Setup build environment
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y \
|
||||
autoconf \
|
||||
automake \
|
||||
build-essential \
|
||||
clang-18 \
|
||||
curl \
|
||||
git \
|
||||
libarchive-tools \
|
||||
libtool \
|
||||
lld-18 \
|
||||
makepkg \
|
||||
meson \
|
||||
pacman-package-manager \
|
||||
pkg-config \
|
||||
xxd \
|
||||
zip
|
||||
|
||||
- name: Build PS5 payload SDK
|
||||
working-directory: pacbrew-repo/sdk
|
||||
run: |
|
||||
makepkg -c -f
|
||||
sudo pacman --noconfirm -U ./ps5-payload-*.pkg.tar.gz
|
||||
|
||||
- name: Build PS5 sqlite package
|
||||
working-directory: pacbrew-repo/sqlite
|
||||
run: |
|
||||
makepkg -c -f
|
||||
sudo pacman --noconfirm -U ./ps5-payload-*.pkg.tar.gz
|
||||
|
||||
- name: Build payload
|
||||
run: |
|
||||
make clean all PS5_SCE_STUBS_DIR="$GITHUB_WORKSPACE/ps5-sdk/sce_stubs"
|
||||
|
||||
- name: Create release archive
|
||||
id: package
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version_tag="$(git describe --abbrev=6 --dirty --always --tags 2>/dev/null || echo "${GITHUB_SHA::7}")"
|
||||
archive_name="ShadowMountPlus_${version_tag}.zip"
|
||||
manifest_file=".github/release-files.txt"
|
||||
|
||||
mapfile -t release_files < <(grep -Ev '^[[:space:]]*(#|$)' "${manifest_file}")
|
||||
|
||||
if [ "${#release_files[@]}" -eq 0 ]; then
|
||||
echo "Release manifest is empty: ${manifest_file}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for release_file in "${release_files[@]}"; do
|
||||
if [ ! -f "${release_file}" ]; then
|
||||
echo "Missing release file: ${release_file}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
zip -9 "${archive_name}" "${release_files[@]}"
|
||||
|
||||
echo "archive_name=${archive_name}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ steps.package.outputs.archive_name }}
|
||||
path: ${{ steps.package.outputs.archive_name }}
|
||||
if-no-files-found: error
|
||||
|
||||
release:
|
||||
needs: build
|
||||
permissions:
|
||||
contents: write
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ needs.build.outputs.archive_name }}
|
||||
path: dist
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.ref_name }}
|
||||
generate_release_notes: true
|
||||
files: dist/${{ needs.build.outputs.archive_name }}
|
||||
@@ -3,3 +3,6 @@
|
||||
daemon.lock
|
||||
debug.log
|
||||
notify.txt
|
||||
src/notify_icon_asset.c
|
||||
refs/*
|
||||
docs/*
|
||||
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
@@ -1,21 +1,47 @@
|
||||
PS5_PAYLOAD_SDK ?= /opt/ps5-payload-sdk
|
||||
include $(PS5_PAYLOAD_SDK)/toolchain/prospero.mk
|
||||
|
||||
# Standard Flags (No extra libraries)
|
||||
CFLAGS := -O2 -Wall -D_BSD_SOURCE -std=gnu11 -Isrc -I$(INCDIR)
|
||||
|
||||
# Linker
|
||||
LDFLAGS := -L$(LIBdir)
|
||||
|
||||
# Standard Libraries Only
|
||||
LIBS := -lkernel_sys -lSceSystemService -lSceUserService -lSceAppInstUtil
|
||||
|
||||
# Targets
|
||||
all: shadowmount.elf
|
||||
|
||||
# Build Daemon
|
||||
shadowmount.elf: src/main.c
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $< $(LIBS)
|
||||
|
||||
clean:
|
||||
rm -f shadowmount.elf kill.elf src/*.o
|
||||
PS5_PAYLOAD_SDK ?= /opt/ps5-payload-sdk
|
||||
include $(PS5_PAYLOAD_SDK)/toolchain/prospero.mk
|
||||
|
||||
VERSION_TAG := $(shell git describe --abbrev=6 --dirty --always --tags 2>/dev/null || echo unknown)
|
||||
|
||||
# Standard Flags (No extra libraries)
|
||||
CFLAGS := -O3 -flto=thin -DNDEBUG -ffunction-sections -fdata-sections -Wall -Wextra -Wstrict-prototypes -Wmissing-prototypes -Werror=strict-prototypes -Werror=missing-prototypes -D_BSD_SOURCE -std=gnu11 -Iinclude -Isrc
|
||||
CFLAGS += -DSHADOWMOUNT_VERSION=\"$(VERSION_TAG)\"
|
||||
|
||||
# Linker
|
||||
LDFLAGS := -flto=thin -Wl,--gc-sections
|
||||
|
||||
# Standard Libraries Only
|
||||
LIBS := -lSceNotification -lSceSystemService -lSceUserService -lSceAppInstUtil -lsqlite3
|
||||
PS5_SCE_STUBS_DIR ?= $(PS5_PAYLOAD_SDK)/src/sce_stubs
|
||||
KERNEL_SYS_STUB_SO := src/libkernel_sys_ext.so
|
||||
KERNEL_SYS_STUB_SRCS := $(PS5_SCE_STUBS_DIR)/libkernel_sys.c src/libkernel_sys_ext.c
|
||||
|
||||
ASSET_SRCS := src/notify_icon_asset.c src/config_ini_example_asset.c
|
||||
SRCS := src/main.c $(wildcard src/sm_*.c) $(ASSET_SRCS)
|
||||
OBJS := $(SRCS:.c=.o)
|
||||
HEADERS := $(wildcard include/*.h)
|
||||
|
||||
# Targets
|
||||
all: shadowmountplus.elf
|
||||
|
||||
# Build Daemon
|
||||
shadowmountplus.elf: $(OBJS) $(KERNEL_SYS_STUB_SO)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJS) $(KERNEL_SYS_STUB_SO) $(LIBS)
|
||||
$(PS5_PAYLOAD_SDK)/bin/prospero-strip --strip-all $@
|
||||
rm -f src/notify_icon_asset.c src/config_ini_example_asset.c
|
||||
|
||||
$(KERNEL_SYS_STUB_SO): $(KERNEL_SYS_STUB_SRCS)
|
||||
test -f "$(PS5_SCE_STUBS_DIR)/libkernel_sys.c"
|
||||
$(CC) -shared -Wl,-soname=libkernel_sys.sprx -o $@ $^
|
||||
|
||||
src/notify_icon_asset.c: smp_icon.png
|
||||
xxd -i $< > $@
|
||||
|
||||
src/config_ini_example_asset.c: config.ini.example
|
||||
xxd -i $< > $@
|
||||
|
||||
src/%.o: src/%.c $(HEADERS)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
clean:
|
||||
rm -f shadowmountplus.elf kill.elf src/*.o $(KERNEL_SYS_STUB_SO) src/notify_icon_asset.c src/config_ini_example_asset.c
|
||||
@@ -0,0 +1,399 @@
|
||||
<# New-OsfExfatImage.ps1
|
||||
|
||||
PURPOSE
|
||||
-------
|
||||
Create a RAW image file, mount it via OSFMount as a logical volume,
|
||||
format it as exFAT, and either:
|
||||
- format + copy + dismount (default), or
|
||||
- create + mount only for manual steps.
|
||||
|
||||
USAGE (run PowerShell as Administrator)
|
||||
--------------------------------------
|
||||
1) Auto-size (recommended):
|
||||
powershell.exe -ExecutionPolicy Bypass -File .\New-OsfExfatImage.ps1 `
|
||||
-ImagePath "C:\images\data.img" `
|
||||
-SourceDir "C:\payload" `
|
||||
-Label "DATA" `
|
||||
-ForceOverwrite
|
||||
|
||||
2) Fixed size:
|
||||
powershell.exe -ExecutionPolicy Bypass -File .\New-OsfExfatImage.ps1 `
|
||||
-ImagePath "C:\images\data.img" `
|
||||
-SourceDir "C:\payload" `
|
||||
-Size 8G `
|
||||
-Label "DATA" `
|
||||
-ForceOverwrite
|
||||
|
||||
3) Create empty image and keep mounted (manual format/copy):
|
||||
powershell.exe -ExecutionPolicy Bypass -File .\New-OsfExfatImage.ps1 `
|
||||
-ImagePath "C:\images\data.exfat" `
|
||||
-SourceDir "C:\payload\APPXXXX" `
|
||||
-CreateEmptyAndMount `
|
||||
-ForceOverwrite
|
||||
|
||||
PARAMETERS
|
||||
----------
|
||||
-ImagePath Output image file path.
|
||||
-SourceDir Folder to copy into the new volume.
|
||||
-Size Optional. If omitted, an optimal size is computed to fit all files.
|
||||
Suffixes: K/M/G/T (1024), k/m/g/t (1000), b (512-byte blocks), or bytes.
|
||||
-Label Volume label.
|
||||
-ForceOverwrite Recreate image if it already exists.
|
||||
-CreateEmptyAndMount
|
||||
Create and mount image only. Skip format/copy and leave mounted.
|
||||
|
||||
NOTES
|
||||
-----
|
||||
- This script does NOT auto-elevate. Start PowerShell as Administrator.
|
||||
- Filesystem is always exFAT.
|
||||
- Cluster size is auto-selected:
|
||||
- large-file sets: 65536
|
||||
- small/mixed-file sets: 32768
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ImagePath,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SourceDir,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$Size,
|
||||
|
||||
[string]$Label = "OSFIMG",
|
||||
|
||||
[switch]$ForceOverwrite,
|
||||
|
||||
[switch]$CreateEmptyAndMount
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
$scriptCulture = [System.Globalization.CultureInfo]::GetCultureInfo("en-US")
|
||||
[System.Threading.Thread]::CurrentThread.CurrentCulture = $scriptCulture
|
||||
[System.Threading.Thread]::CurrentThread.CurrentUICulture = $scriptCulture
|
||||
[System.Globalization.CultureInfo]::DefaultThreadCurrentCulture = $scriptCulture
|
||||
[System.Globalization.CultureInfo]::DefaultThreadCurrentUICulture = $scriptCulture
|
||||
|
||||
function Test-Admin {
|
||||
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$p = New-Object Security.Principal.WindowsPrincipal($id)
|
||||
return $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
function Find-OSFMountCom {
|
||||
$cmd = Get-Command "osfmount.com" -ErrorAction SilentlyContinue
|
||||
if ($cmd) { return $cmd.Source }
|
||||
|
||||
$candidates = @(
|
||||
"${env:ProgramFiles}\OSFMount\osfmount.com",
|
||||
"${env:ProgramFiles(x86)}\OSFMount\osfmount.com",
|
||||
"${env:ProgramFiles}\PassMark\OSFMount\osfmount.com",
|
||||
"${env:ProgramFiles(x86)}\PassMark\OSFMount\osfmount.com"
|
||||
) | Where-Object { $_ -and (Test-Path $_) }
|
||||
|
||||
$candidates = @($candidates)
|
||||
if ($candidates.Count -gt 0) { return $candidates[0] }
|
||||
|
||||
throw "osfmount.com not found. Add OSFMount to PATH or install it to a standard location."
|
||||
}
|
||||
|
||||
function Parse-SizeToBytes([string]$s) {
|
||||
$s = $s.Trim()
|
||||
if ($s -match '^\s*(\d+)\s*([bBkKmMgGtT]?)\s*$') {
|
||||
$num = [Int64]$matches[1]
|
||||
$u = $matches[2]
|
||||
switch ($u) {
|
||||
'' { return $num }
|
||||
'b' { return $num * 512 }
|
||||
'B' { return $num * 512 }
|
||||
'K' { return $num * 1024 }
|
||||
'M' { return $num * 1024 * 1024 }
|
||||
'G' { return $num * 1024 * 1024 * 1024 }
|
||||
'T' { return $num * 1024 * 1024 * 1024 * 1024 }
|
||||
'k' { return $num * 1000 }
|
||||
'm' { return $num * 1000 * 1000 }
|
||||
'g' { return $num * 1000 * 1000 * 1000 }
|
||||
't' { return $num * 1000 * 1000 * 1000 * 1000 }
|
||||
default { throw "Unknown size suffix: '$u'" }
|
||||
}
|
||||
}
|
||||
throw "Failed to parse size string: '$s'"
|
||||
}
|
||||
|
||||
function Format-Bytes([Int64]$bytes) {
|
||||
if ($bytes -ge 1TB) { return "{0:N2} TB" -f ($bytes/1TB) }
|
||||
if ($bytes -ge 1GB) { return "{0:N2} GB" -f ($bytes/1GB) }
|
||||
if ($bytes -ge 1MB) { return "{0:N2} MB" -f ($bytes/1MB) }
|
||||
if ($bytes -ge 1KB) { return "{0:N2} KB" -f ($bytes/1KB) }
|
||||
return "$bytes B"
|
||||
}
|
||||
|
||||
function Get-FreeDriveLetter {
|
||||
$used = (Get-PSDrive -PSProvider FileSystem).Name
|
||||
foreach ($code in 68..90) {
|
||||
$letter = [char]$code
|
||||
if ($used -notcontains [string]$letter) { return [string]$letter }
|
||||
}
|
||||
throw "No free drive letters available (D:..Z:)."
|
||||
}
|
||||
|
||||
function Get-OptimalImageSizeBytes([string]$dir, [int]$clusterBytes) {
|
||||
$cluster = [Int64]$clusterBytes
|
||||
[Int64]$metaFixed = 32MB
|
||||
[Int64]$minSlack = 64MB
|
||||
[Int64]$spareMin = 64MB
|
||||
[Int64]$spareMax = 512MB
|
||||
[Int64]$entryMetaBytes = 256
|
||||
|
||||
$files = @(Get-ChildItem -LiteralPath $dir -Recurse -File -Force)
|
||||
$dirs = @(Get-ChildItem -LiteralPath $dir -Recurse -Directory -Force)
|
||||
|
||||
[Int64]$rawFileBytes = 0
|
||||
[Int64]$dataBytes = 0
|
||||
foreach ($f in $files) {
|
||||
$len = [Int64]$f.Length
|
||||
$rawFileBytes += $len
|
||||
$dataBytes += [Int64]([Math]::Ceiling($len / [double]$cluster) * $cluster)
|
||||
}
|
||||
|
||||
[Int64]$dataClusters = [Int64]([Math]::Ceiling($dataBytes / [double]$cluster))
|
||||
[Int64]$fatBytes = $dataClusters * 4
|
||||
[Int64]$bitmapBytes = [Int64]([Math]::Ceiling($dataClusters / 8.0))
|
||||
[Int64]$entryBytes =
|
||||
(([Int64]$files.Count + [Int64]$dirs.Count) * $entryMetaBytes)
|
||||
|
||||
[Int64]$baseTotal =
|
||||
$dataBytes + $fatBytes + $bitmapBytes + $entryBytes + $metaFixed
|
||||
[Int64]$spareBytes = [Int64]([Math]::Ceiling($baseTotal / 200.0))
|
||||
if ($spareBytes -lt $spareMin) { $spareBytes = $spareMin }
|
||||
if ($spareBytes -gt $spareMax) { $spareBytes = $spareMax }
|
||||
[Int64]$total = $baseTotal + $spareBytes
|
||||
[Int64]$minTotal = $rawFileBytes + $minSlack
|
||||
if ($total -lt $minTotal) { $total = $minTotal }
|
||||
|
||||
[Int64]$align = 1MB
|
||||
$total = [Int64]([Math]::Ceiling($total / [double]$align) * $align)
|
||||
return $total
|
||||
}
|
||||
|
||||
function Wait-ForLogicalDrive([string]$driveLetter, [int]$timeoutSeconds = 20) {
|
||||
$target = "${driveLetter}:"
|
||||
$sw = [Diagnostics.Stopwatch]::StartNew()
|
||||
while ($sw.Elapsed.TotalSeconds -lt $timeoutSeconds) {
|
||||
$logical = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DeviceID='$target'" -ErrorAction SilentlyContinue
|
||||
if ($logical) { return $true }
|
||||
Start-Sleep -Milliseconds 300
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function Get-LogicalDriveFileSystem([string]$driveLetter) {
|
||||
$target = "${driveLetter}:"
|
||||
$logical = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DeviceID='$target'" -ErrorAction SilentlyContinue
|
||||
if ($logical) { return [string]$logical.FileSystem }
|
||||
return ""
|
||||
}
|
||||
|
||||
function Get-OptimalExfatClusterSize([string]$dir) {
|
||||
[Int64]$largeFileThreshold = 1MB
|
||||
|
||||
$files = @(Get-ChildItem -LiteralPath $dir -Recurse -File -Force)
|
||||
if ($files.Count -eq 0) { return 32768 }
|
||||
|
||||
[Int64]$rawFileBytes = 0
|
||||
foreach ($f in $files) {
|
||||
$rawFileBytes += [Int64]$f.Length
|
||||
}
|
||||
|
||||
[Int64]$avgFileBytes = [Int64]($rawFileBytes / [Int64]$files.Count)
|
||||
if ($avgFileBytes -ge $largeFileThreshold) { return 65536 }
|
||||
return 32768
|
||||
}
|
||||
|
||||
function Format-AllocationUnitArg([int]$clusterSize) {
|
||||
if ($clusterSize -ge 1MB -and ($clusterSize % 1MB) -eq 0) {
|
||||
return "{0}M" -f ($clusterSize / 1MB)
|
||||
}
|
||||
if ($clusterSize -ge 1KB -and ($clusterSize % 1KB) -eq 0) {
|
||||
return "{0}K" -f ($clusterSize / 1KB)
|
||||
}
|
||||
return "$clusterSize"
|
||||
}
|
||||
|
||||
function Dismount-OsfVolume([string]$osfPath, [string]$mountPoint, [int]$maxAttempts = 6) {
|
||||
if ([string]::IsNullOrWhiteSpace($mountPoint)) { return $false }
|
||||
|
||||
$targets = @($mountPoint)
|
||||
if (-not $mountPoint.EndsWith("\")) { $targets += "$mountPoint\" }
|
||||
|
||||
for ($i = 1; $i -le $maxAttempts; $i++) {
|
||||
foreach ($target in $targets) {
|
||||
try {
|
||||
& $osfPath -d -m $target 2>&1 | Out-Null
|
||||
if ($LASTEXITCODE -eq 0) { return $true }
|
||||
} catch {
|
||||
# Retry: volume can remain busy for a short time after format/copy.
|
||||
}
|
||||
}
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
|
||||
function Invoke-FormatVolume([string]$driveLetter, [int]$clusterSize, [string]$label) {
|
||||
$target = "${driveLetter}:"
|
||||
$fileSystem = "exFAT"
|
||||
$clusterArg = Format-AllocationUnitArg -clusterSize $clusterSize
|
||||
$attempts = @(
|
||||
@{ Name = "$fileSystem quick with requested allocation unit"; Args = @($target, "/FS:$fileSystem", "/A:$clusterArg", "/Q", "/V:$label", "/X", "/Y") }
|
||||
)
|
||||
|
||||
$lastFormatExitCode = -1
|
||||
foreach ($attempt in $attempts) {
|
||||
Write-Host "[Info] format attempt: $($attempt.Name)"
|
||||
$stdoutPath = [System.IO.Path]::GetTempFileName()
|
||||
$stderrPath = [System.IO.Path]::GetTempFileName()
|
||||
try {
|
||||
$proc = Start-Process -FilePath "format.com" -ArgumentList $attempt.Args -Wait -PassThru -NoNewWindow `
|
||||
-RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath
|
||||
$lastFormatExitCode = [int]$proc.ExitCode
|
||||
|
||||
# Show native format.com output in current console.
|
||||
if (Test-Path -LiteralPath $stdoutPath) {
|
||||
Get-Content -LiteralPath $stdoutPath -ErrorAction SilentlyContinue | ForEach-Object { Write-Host $_ }
|
||||
}
|
||||
if (Test-Path -LiteralPath $stderrPath) {
|
||||
Get-Content -LiteralPath $stderrPath -ErrorAction SilentlyContinue | ForEach-Object { Write-Host $_ }
|
||||
}
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Some environments may report a non-zero exit code even when formatting completed.
|
||||
# Validate resulting filesystem as the source of truth.
|
||||
if (Wait-ForLogicalDrive -driveLetter $driveLetter -timeoutSeconds 5) {
|
||||
$actualFs = Get-LogicalDriveFileSystem -driveLetter $driveLetter
|
||||
if ($actualFs -and $actualFs.ToUpperInvariant() -eq $fileSystem.ToUpperInvariant()) {
|
||||
Write-Host "[Info] format result: detected filesystem '$actualFs' on $target."
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "[Info] format attempt failed (exit code $lastFormatExitCode), retrying..."
|
||||
}
|
||||
|
||||
throw "format.com failed for $target after all retry strategies. Last exit code: $lastFormatExitCode"
|
||||
}
|
||||
|
||||
# -------------------- Main --------------------
|
||||
|
||||
if (-not (Test-Admin)) { throw "Please run PowerShell as Administrator." }
|
||||
if (-not (Test-Path $SourceDir -PathType Container)) { throw "Source directory not found: $SourceDir" }
|
||||
if (-not (Test-Path (Join-Path $SourceDir "eboot.bin") -PathType Leaf)) { throw "eboot.bin not found in source directory: $SourceDir" }
|
||||
|
||||
# Ensure output directory exists
|
||||
$outDir = Split-Path -Parent $ImagePath
|
||||
if ($outDir -and -not (Test-Path $outDir)) {
|
||||
New-Item -ItemType Directory -Path $outDir | Out-Null
|
||||
}
|
||||
|
||||
if (Test-Path $ImagePath) {
|
||||
if (-not $ForceOverwrite) { throw "Image file already exists: $ImagePath. Use -ForceOverwrite to replace it." }
|
||||
Remove-Item -Force $ImagePath
|
||||
}
|
||||
|
||||
[Int64]$expectedBytes = 0
|
||||
[string]$osfSizeArg = $null
|
||||
[int]$ExfatClusterSize = Get-OptimalExfatClusterSize -dir $SourceDir
|
||||
[bool]$sizeProvided = -not [string]::IsNullOrWhiteSpace($Size)
|
||||
[int]$TargetClusterSize = $ExfatClusterSize
|
||||
|
||||
if (-not $sizeProvided) {
|
||||
Write-Host "[Info] Size not provided. Computing an optimal image size from '$SourceDir'..."
|
||||
$expectedBytes = Get-OptimalImageSizeBytes -dir $SourceDir -clusterBytes $TargetClusterSize
|
||||
$osfSizeArg = "$expectedBytes"
|
||||
} else {
|
||||
$expectedBytes = Parse-SizeToBytes $Size
|
||||
$osfSizeArg = $Size
|
||||
}
|
||||
|
||||
if (-not $sizeProvided) {
|
||||
Write-Host "[Info] Computed image size: $(Format-Bytes $expectedBytes) ($expectedBytes bytes)."
|
||||
}
|
||||
Write-Host "[Info] Selected filesystem: exFAT (cluster=$TargetClusterSize) for image size $(Format-Bytes $expectedBytes)."
|
||||
|
||||
$osf = Find-OSFMountCom
|
||||
|
||||
[string]$DriveLetter = ""
|
||||
[string]$MountPoint = ""
|
||||
[bool]$Mounted = $false
|
||||
[bool]$LeaveMounted = $CreateEmptyAndMount.IsPresent
|
||||
|
||||
try {
|
||||
$DriveLetter = Get-FreeDriveLetter
|
||||
$MountPoint = "${DriveLetter}:"
|
||||
|
||||
if ($CreateEmptyAndMount) {
|
||||
Write-Host "[1/2] Creating & mounting the image via OSFMount as a logical volume on $MountPoint ..."
|
||||
} else {
|
||||
Write-Host "[1/4] Creating & mounting the image via OSFMount as a logical volume on $MountPoint ..."
|
||||
}
|
||||
$out = & $osf -a -t file -f $ImagePath -s $osfSizeArg -m $MountPoint -o rw,rem 2>&1
|
||||
Write-Host ($out | Out-String).Trim()
|
||||
if ($LASTEXITCODE -ne 0) { throw "osfmount.com failed with exit code $LASTEXITCODE." }
|
||||
$Mounted = $true
|
||||
if (-not (Wait-ForLogicalDrive -driveLetter $DriveLetter -timeoutSeconds 20)) {
|
||||
throw "Mounted drive $MountPoint did not appear in time."
|
||||
}
|
||||
|
||||
$dest = "${DriveLetter}:\"
|
||||
if ($CreateEmptyAndMount) {
|
||||
Write-Host "[2/2] Done. Empty image is mounted at $dest."
|
||||
Write-Host "Manual steps:"
|
||||
Write-Host " 1) Format $dest as exFAT (recommended cluster: 64KB for large-file sets, 32KB for small/mixed sets)."
|
||||
Write-Host " 2) Copy contents of '$SourceDir' to $dest."
|
||||
Write-Host " 3) Dismount: `"$osf`" -d -m $MountPoint"
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "[2/4] Formatting $dest as exFAT (cluster=$TargetClusterSize, label='$Label') via format.com ..."
|
||||
Invoke-FormatVolume -driveLetter $DriveLetter -clusterSize $TargetClusterSize -label $Label
|
||||
|
||||
if (-not (Test-Path $dest)) { throw "Drive $dest is not accessible after formatting." }
|
||||
|
||||
Write-Host "[3/4] Copying contents of '$SourceDir' -> '$dest' ..."
|
||||
$roboArgs = @(
|
||||
$SourceDir, $dest,
|
||||
"/E", "/COPY:DAT", "/DCOPY:DAT",
|
||||
"/R:1", "/W:1",
|
||||
"/NFL", "/NDL",
|
||||
"/ETA"
|
||||
)
|
||||
& robocopy.exe @roboArgs
|
||||
$robocopyExitCode = $LASTEXITCODE
|
||||
if ($robocopyExitCode -gt 7) { throw "robocopy failed. Exit code: $robocopyExitCode" }
|
||||
|
||||
Write-Host "[4/4] Done. Dismounting OSFMount volume..."
|
||||
}
|
||||
finally {
|
||||
if ($Mounted -and -not $LeaveMounted -and -not [string]::IsNullOrWhiteSpace($MountPoint)) {
|
||||
try {
|
||||
$currentPath = (Get-Location).Path
|
||||
if ($currentPath -like "$MountPoint*") {
|
||||
Set-Location "$env:SystemDrive\"
|
||||
}
|
||||
} catch {
|
||||
# Best effort only.
|
||||
}
|
||||
|
||||
if (-not (Dismount-OsfVolume -osfPath $osf -mountPoint $MountPoint -maxAttempts 6)) {
|
||||
Write-Warning "Failed to dismount OSFMount volume ($MountPoint): access denied or volume busy."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "OK: Image created at: $ImagePath"
|
||||
@@ -1,68 +1,397 @@
|
||||
# ShadowMount (PS5)
|
||||
# ShadowMountPlus (PS5)
|
||||
|
||||
**Repository:** https://github.com/drakmor/shadowMountPlus
|
||||
|
||||
**Discord:** https://discord.gg/x2Ppvzwjhm
|
||||
|
||||
|
||||
Thanks for ffpkg support: @Gezine, @earthonion and @VoidWhisper for ShadowMount
|
||||
**Warning! Mounting images can cause shutdown problems and data corruption on internal drives! This depends on many factors, but is more common with older firmware versions. Please take this into account when testing.**
|
||||
|
||||
|
||||
**ShadowMount** is a fully automated, background "Auto-Mounter" payload for Jailbroken PlayStation 5 consoles. It streamlines the game mounting process by eliminating the need for manual configuration or external tools (such as DumpRunner or Itemzflow). ShadowMount automatically detects, mounts, and installs game dumps from both **internal and external storage**.
|
||||
**ShadowMountPlus** is a fully automated, background "Auto-Mounter" payload for Jailbroken PlayStation 5 consoles. It streamlines the game mounting process by eliminating the need for manual configuration or external tools (such as DumpRunner or Itemzflow). ShadowMountPlus automatically detects, mounts, and installs game dumps from both **internal and external storage**.
|
||||
|
||||
|
||||
**Compatibility:** Supports all Jailbroken PS5 firmwares running **[Kstuff-lite v1.07+](https://github.com/EchoStretch/kstuff-lite)**.
|
||||
|
||||
|
||||
## 💜 Support Development
|
||||
|
||||
If you want to support this project, you can donate
|
||||
- USDT (TRC-20): **`TKaUGEwMm9KBXzEoiaaKYBX2yCHAKASW3p`**
|
||||
- USDT (ERC-20): **`0x313dD245dBA957A5560618eA882d08e66aaFb430`**
|
||||
- USDC (Solana): **`5kv7j2RbUGaSP1kU1cZWj9jHH7d6rfvxmK6YXTYbH4um`**
|
||||
|
||||
**Compatibility:** Supports all Jailbroken PS5 firmwares running **Kstuff v1.6.7**.
|
||||
|
||||
|
||||
## Current image support
|
||||
|
||||
`UFS/PFS support is experimental.`
|
||||
`PFS support is experimental.`
|
||||
|
||||
| Extension | Mounted FS | Attach backend | Status |
|
||||
| --- | --- | --- | --- |
|
||||
| `.exfat` | `exfatfs` | `/dev/mdctl` by default | Recommended |
|
||||
| `.ffpkg` | `ufs` | `/dev/lvdctl` | Experimental |
|
||||
| `.ffpfs` | `pfs` | `/dev/lvdctl` | Experimental |
|
||||
| `.ffpkg` | `ufs` | `LVD` or `MD` (configurable) | Recommended |
|
||||
| `.exfat` | `exfatfs` | `LVD` or `MD` (configurable) | Compatibility / external-drive-only titles |
|
||||
| `.ffpfs` | `pfs` | `LVD` | Experimental |
|
||||
| `.ffpfsc` | `pfs` container | `LVD` | Experimental container for nested images |
|
||||
|
||||
Notes:
|
||||
- `.exfat` can be switched to LVD backend by changing `EXFAT_ATTACH_USE_MDCTL` (not working)
|
||||
- PFS mount uses a shell-like profile (`budgetid/mkeymode/sigverify/playgo/disc`) from code defaults.
|
||||
- Backend, read-only mode, and sector size can be configured via `/data/shadowmount/config.ini`.
|
||||
- Debug logging is enabled by default (`debug=1`) and writes to console plus `/data/shadowmount/debug.log` (set `debug=0` to disable).
|
||||
- **UFS (`.ffpkg`) is the recommended image format for normal use.**
|
||||
- **Use exFAT (`.exfat`) only for titles that need external-drive-style compatibility.**
|
||||
- **When building exFAT images manually, keep the cluster size at `64 KB`; smaller clusters can reduce performance.**
|
||||
|
||||
## Recommended FS choice
|
||||
|
||||
- Prefer **UFS (`.ffpkg`)** in most cases: this is the recommended default image format for ShadowMountPlus.
|
||||
- Use **exFAT (`.exfat`)** only for games that do not work correctly unless they are handled like external-drive content.
|
||||
- If you create an **exFAT (`.exfat`)** image manually, use a **`64 KB` cluster size**. Smaller clusters can cause a noticeable performance loss.
|
||||
|
||||
## Runtime config (`/data/shadowmount/config.ini`)
|
||||
|
||||
This file is optional. If it does not exist, ShadowMountPlus creates it from the bundled `config.ini.example` template on startup and uses built-in defaults until you uncomment overrides.
|
||||
|
||||
Supported keys (all optional):
|
||||
- `debug=1|0` (`1` enables `log_debug` output to console + `/data/shadowmount/debug.log`; default is `1`)
|
||||
- `quiet_mode=1|0` (`1` suppresses plain informational popups but keeps rich toasts; default is `0`)
|
||||
- `mount_read_only=1|0` (default: `1`)
|
||||
- `force_mount=1|0` (mounting even damaged file systems; default: `0`)
|
||||
- `app_install_all=1|0` (`1` stages new titles and submits them through `sceAppInstUtilAppInstallAll`; default: `0` on FW below `12.00`, forced `1` on FW `12.00+`)
|
||||
- `image_ro=<image_filename>` (repeatable; force read-only mode for this image filename)
|
||||
- `image_rw=<image_filename>` (repeatable; force read-write mode for this image filename)
|
||||
- `image_sector=<image_filename>:<sector_size>` (repeatable; force sector size for this image filename)
|
||||
- `scan_depth=<1..2>` (`1` = scan only first-level subfolders, `2` = also scan one additional nested level; default: `1`)
|
||||
- `watch_subdirs=1|0` (`1` keeps vnode watchers on scan root subdirectories; default: `0`)
|
||||
- `watch_image_files=1|0` (`1` keeps vnode watchers on discovered image files; default: `1`)
|
||||
- `recursive_scan=1|0` (deprecated compatibility key; `1` forces `scan_depth=2`)
|
||||
- `scan_interval_seconds=<1..3600>` (full scan loop interval; default: `15`)
|
||||
- `stability_wait_seconds=<0..3600>` (minimum source age before processing; default: `10`)
|
||||
- `exfat_backend=lvd|md` (default: `lvd`)
|
||||
- `ufs_backend=lvd|md` (default: `lvd`)
|
||||
- `backport_fakelib=1|0` (`1` mounts sandbox `fakelib` overlays for running games; default: `1`)
|
||||
- `global_fakelib=1|0` (`1` enables the global fakelib overlay when the folder exists; default: `1`)
|
||||
- `global_fakelib_path=<absolute_path>` (global fakelib folder; default: `/data/shadowmount/fakelib`)
|
||||
- `global_fakelib_priority=game|global` (overlay priority when both global and game fakelib exist; default: `game`)
|
||||
- `global_fakelib_exclude=<TITLE_ID>` (repeatable; disables the global fakelib overlay for matching titles)
|
||||
- `kstuff_game_auto_toggle=1|0` (`1` pauses kstuff after tracked game launches and resumes it on stop; default: `1`)
|
||||
- `kstuff_crash_detection=1|0` (`1` enables crash monitoring and pause-delay autotune updates; default: `1`)
|
||||
- `kstuff_pause_delay_image_seconds=<0..3600>` (delay before pausing kstuff for image-backed launches; default: `25`)
|
||||
- `kstuff_pause_delay_direct_seconds=<0..3600>` (delay before pausing kstuff for direct/non-image launches; default: `15`)
|
||||
- `kstuff_no_pause=<TITLE_ID>` (repeatable; keeps kstuff enabled for matching titles)
|
||||
- `kstuff_delay=<TITLE_ID>:<0..3600>` (repeatable; per-title pause delay override, last matching rule wins)
|
||||
- `/data/shadowmount/autotune.ini` may also provide per-title pause-delay overrides with highest priority:
|
||||
- `kstuff_delay=<TITLE_ID>:<0..3600>`
|
||||
- `<TITLE_ID>=<0..3600>`
|
||||
- `image_sector=<image_filename>:<sector_size>`
|
||||
- `scanpath=<absolute_path>` (can be repeated on multiple lines; default: built-in scan path list below)
|
||||
- `lvd_exfat_sector_size=<value>` (default: `512`)
|
||||
- `lvd_ufs_sector_size=<value>` (default: `4096`)
|
||||
- `lvd_pfs_sector_size=<value>` (default: `32768`)
|
||||
- `md_exfat_sector_size=<value>` (default: `512`)
|
||||
- `md_ufs_sector_size=<value>` (default: `512`)
|
||||
|
||||
Per-image mode override behavior:
|
||||
- Match is done by image file name (without path).
|
||||
- File names with spaces are supported.
|
||||
- If multiple rules target the same file name, the last one in config wins.
|
||||
- If no rule matches, global `mount_read_only` is used.
|
||||
- Example:
|
||||
```ini
|
||||
mount_read_only=1
|
||||
image_rw=PPSA1234-my-image.ffpfs
|
||||
image_rw=MYGame 123.exfat
|
||||
image_ro=legacy_dump.ffpkg
|
||||
image_sector=MYGame 123.exfat:65536
|
||||
```
|
||||
|
||||
Per-image sector override behavior:
|
||||
- Match is done by image file name (without path).
|
||||
- `image_sector` in `/data/shadowmount/autotune.ini` has the highest priority for matching image files.
|
||||
- If no per-image rule matches, the backend-specific global sector size defaults are used.
|
||||
- When image validation fails because the mounted file-system cluster size is smaller than the selected device sector size, ShadowMountPlus writes `image_sector=<image_filename>:<cluster_size>` into `/data/shadowmount/autotune.ini` and asks you to try mounting again.
|
||||
|
||||
Scan path behavior:
|
||||
- If at least one `scanpath=...` is present, only those custom paths are used.
|
||||
- `/mnt/shadowmnt/pfsc` and `/mnt/shadowmnt` are always added automatically, even with custom paths.
|
||||
- With `scan_depth=1` (default), only first-level subfolders are checked.
|
||||
- With `scan_depth=2`, one additional nested level is checked.
|
||||
- If `recursive_scan=1` is set, ShadowMount+ forces `scan_depth=2`.
|
||||
- Full scan loop runs every `scan_interval_seconds` (default: `15`).
|
||||
- Sources newer than `stability_wait_seconds` are deferred until stable (default: `10`).
|
||||
- Direct folder installs use `<game>/sce_sys` for this check; image and backport sources use the target path itself.
|
||||
|
||||
Backport overlay behavior:
|
||||
- For each `scanpath`, use:
|
||||
- `<scanpath>/backports/<TITLE_ID>/`
|
||||
- The `backports` folder is ignored during normal game scanning.
|
||||
- A backport is applied automatically to the matching mounted game from any configured scan path.
|
||||
- If multiple scan paths provide the same title backport, the game's own scan path wins; otherwise scan path order is used.
|
||||
- If `/mnt/sandbox/<TITLE_ID>_XXX/app0/fakelib` exists while the game is running, ShadowMount+ mounts it into that game's sandbox `common/lib`.
|
||||
- If `global_fakelib=1` and `global_fakelib_path` exists as a directory, ShadowMount+ also mounts that folder into the same sandbox `common/lib`.
|
||||
- When both global and per-game fakelib exist, the default gives the game's own `fakelib` priority by mounting `/data/shadowmount/fakelib` first and then the game-specific fakelib.
|
||||
- `global_fakelib_priority=global` reverses that priority.
|
||||
- Use repeatable `global_fakelib_exclude=<TITLE_ID>` entries to skip the global fakelib for specific games without disabling per-game fakelib.
|
||||
- `backport_fakelib=0` disables the sandbox `fakelib` watcher, including global fakelib.
|
||||
- For `backport_fakelib` to work correctly, the standalone `BackPork` payload must be disabled. Running both at the same time will conflict.
|
||||
|
||||
Kstuff game lifecycle behavior:
|
||||
- When `kstuff_game_auto_toggle=1`, ShadowMount watches game `exec/exit` events in the background.
|
||||
- Image-backed launches use `kstuff_pause_delay_image_seconds`; direct/non-image launches use `kstuff_pause_delay_direct_seconds`.
|
||||
- `kstuff_crash_detection=0` disables crash monitoring and the automatic pause-delay tuning logic, while leaving normal kstuff auto-pause/auto-resume behavior intact.
|
||||
- `kstuff_no_pause` skips auto-pause entirely for matching title IDs.
|
||||
- `kstuff_delay` overrides the pause delay for matching title IDs, regardless of image/direct launch type.
|
||||
- `/data/shadowmount/autotune.ini` overrides both `config.ini` and `autopause.txt` for matching title IDs.
|
||||
- `/data/shadowmount/autotune.ini` also overrides `image_sector` rules from `config.ini` for matching image file names.
|
||||
- A game source folder may optionally contain `autopause.txt`; it is read once at launch time.
|
||||
- Priority order is: `autotune.ini` -> `kstuff_delay` from `config.ini` -> `autopause.txt` -> global direct/image default delay.
|
||||
- If `autopause.txt` contains only a number, that value is used for direct launches and doubled for image-backed launches.
|
||||
- `autopause.txt` may also use:
|
||||
- `direct=<seconds>`
|
||||
- `image=<seconds>`
|
||||
- If both kinds of rule target the same title, `kstuff_no_pause` takes priority.
|
||||
- When crash monitoring detects an app crash before kstuff was paused, ShadowMountPlus only notifies that the app crashed and kstuff is not to blame.
|
||||
- When crash monitoring detects an app crash within 2 minutes after kstuff auto-pause, ShadowMountPlus doubles the applied pause delay for that title and upserts it into `/data/shadowmount/autotune.ini` (up to `3600` seconds), then prompts you to launch the game again.
|
||||
- When the last tracked game stops, ShadowMount immediately enables kstuff again if it was the component that disabled it.
|
||||
|
||||
|
||||
Validation:
|
||||
- See `config.ini.example` for a ready-to-use template.
|
||||
|
||||
## Mount point naming
|
||||
|
||||
Image mountpoints are created under:
|
||||
|
||||
`/data/ufsmnt/<image_name>-<fs_suffix>`
|
||||
`/mnt/shadowmnt/<image_name>_<hash>`
|
||||
|
||||
PFSC container mountpoints are created under:
|
||||
|
||||
`/mnt/shadowmnt/pfsc/<image_name>_<hash>`
|
||||
|
||||
Image layout requirement (`.ffpkg`, `.exfat`, `.ffpfs`):
|
||||
- Game files must be placed at the image root.
|
||||
- Do not add an extra top-level folder inside the image.
|
||||
- Valid example: `/sce_sys/param.json` exists directly from image root.
|
||||
- Invalid example: `/GAME_FOLDER/sce_sys/param.json` (extra nesting level).
|
||||
|
||||
PFSC container layout requirement (`.ffpfsc`):
|
||||
- Do not place game files directly in the container root.
|
||||
- Place supported nested image files inside the container; ShadowMountPlus mounts those nested images and scans them for the game.
|
||||
- A nested `pfs_image.dat` file inside a PFSC container is treated as a PFS image.
|
||||
- `.ffpfsc` uses the nested outer PFS profile (`img_type=0x02`); `.ffpfs` and `pfs_image.dat` files mounted from inside it use the nested inner profile (`img_type=0x82`). Signature verification and GDDR5 cache setup are kept in code but currently disabled.
|
||||
|
||||
## Compressed PFS containers (`.ffpfsc`)
|
||||
|
||||
Compressed PFS mode is intended only for nested images. During packing, data is
|
||||
zero-padded to a `64 KB` sector boundary, so the compressed PFS should be used
|
||||
as an outer container for another image rather than as a direct game-file
|
||||
layout.
|
||||
|
||||
Recommended layouts:
|
||||
- exFAT image inside compressed PFS.
|
||||
- Uncompressed PFS image inside compressed PFS.
|
||||
|
||||
MkPFS uses zLib compression. Decompression is hardware-assisted, but throughput
|
||||
is limited to roughly `150-250 MB/s`. This is about one third of the speed of an
|
||||
external USB drive, FFPKG/exFAT images, or about one tenth of the internal drive
|
||||
speed. Keep this in mind when choosing which games to pack. Games that read large
|
||||
amounts of data or stream textures continuously may stutter.
|
||||
|
||||
Use the official [PSBrew/MkPFS](https://github.com/PSBrew/MkPFS) tool to pack
|
||||
PFS images.
|
||||
|
||||
### Packing an uncompressed PFS image into compressed PFS
|
||||
|
||||
First create an uncompressed nested PFS image:
|
||||
|
||||
```bash
|
||||
mkpfs pack folder --verify --no-compress --no-adjust-output-file-extension --version PS5 --inode-bits 32 \
|
||||
'./PPSA07923/PPSA07923-app' \
|
||||
'./pfs_image.dat'
|
||||
```
|
||||
|
||||
Then pack the nested image into a compressed PFS container:
|
||||
|
||||
```bash
|
||||
mkpfs pack file --verify --version PS5 --inode-bits 32 \
|
||||
'./pfs_image.dat' \
|
||||
'./PPSA12345.ffpfsc'
|
||||
```
|
||||
|
||||
After successful packing, the temporary nested image can be removed:
|
||||
|
||||
```bash
|
||||
rm './pfs_image.dat'
|
||||
```
|
||||
|
||||
### Packing an exFAT image into compressed PFS
|
||||
|
||||
First create a normal exFAT image using one of the methods from
|
||||
`Creating an exFAT image`. The nested exFAT image name must keep the `.exfat`
|
||||
extension, for example `PPSA12345.exfat`.
|
||||
|
||||
Linux:
|
||||
|
||||
```bash
|
||||
chmod +x mkexfat.sh
|
||||
./mkexfat.sh ./PPSA12345-app ./PPSA12345.exfat
|
||||
```
|
||||
|
||||
Windows:
|
||||
|
||||
```cmd
|
||||
make_image.bat "C:\images\PPSA12345.exfat" "C:\payload\PPSA12345-app"
|
||||
```
|
||||
|
||||
The exFAT image must contain the game files at the image root, without an extra
|
||||
top-level folder.
|
||||
|
||||
Then pack the exFAT image into a compressed PFS container:
|
||||
|
||||
```bash
|
||||
mkpfs pack file --verify --version PS5 --inode-bits 32 \
|
||||
'./PPSA12345.exfat' \
|
||||
'./PPSA12345.ffpfsc'
|
||||
```
|
||||
|
||||
After successful packing, the temporary exFAT image can be removed:
|
||||
|
||||
```bash
|
||||
rm './PPSA12345.exfat'
|
||||
```
|
||||
|
||||
## Scan paths
|
||||
|
||||
ShadowMount scans these locations:
|
||||
Default scan locations:
|
||||
- `/data/homebrew`
|
||||
- `/data/etaHEN/games`
|
||||
- `/mnt/ext0/etaHEN/homebrew`
|
||||
- `/mnt/ext0/homebrew`
|
||||
- `/mnt/ext0/etaHEN/games`
|
||||
- `/mnt/ext1/etaHEN/homebrew`
|
||||
- `/mnt/ext1/homebrew`
|
||||
- `/mnt/ext1/etaHEN/games`
|
||||
- `/mnt/usb0/homebrew` .. `/mnt/usb7/homebrew`
|
||||
- `/mnt/usb0/etaHEN/games` .. `/mnt/usb7/etaHEN/games`
|
||||
- `/mnt/usb0` .. `/mnt/usb7`
|
||||
- `/mnt/ext0`
|
||||
- `/mnt/ext1`
|
||||
- `/data/ufsmnt` (mounted image content scan)
|
||||
- `/mnt/shadowmnt/pfsc` (mounted PFSC container scan)
|
||||
- `/mnt/shadowmnt` (mounted image content scan)
|
||||
|
||||
You can override scan roots with `scanpath=...` entries in `/data/shadowmount/config.ini`.
|
||||
|
||||
## Manual install list
|
||||
|
||||
For games that should not live under the normal scan paths, ShadowMountPlus also
|
||||
checks:
|
||||
|
||||
`/data/shadowmount/manual.lst`
|
||||
|
||||
Add one source per line:
|
||||
- Path to a game folder, where `sce_sys/param.json` exists inside that folder.
|
||||
- Path to a supported image file: `.ffpkg`, `.exfat`, `.ffpfs`, or `.ffpfsc`.
|
||||
- Empty lines and lines starting with `#` are ignored.
|
||||
|
||||
Example:
|
||||
```text
|
||||
/mnt/usb0/MyGames/PPSA12345
|
||||
/mnt/usb0/images/PPSA54321.ffpkg
|
||||
# /mnt/usb0/disabled/PPSA00000
|
||||
```
|
||||
|
||||
`manual.lst` is watched for changes. When you add a new line, ShadowMountPlus
|
||||
rescans shortly after the write, mounts images when needed, and installs the
|
||||
game through the same pipeline as normal scan path discoveries.
|
||||
|
||||
Manual install state is tracked in:
|
||||
|
||||
`/data/shadowmount/manual.status`
|
||||
|
||||
This status file is managed by ShadowMountPlus. If a manually installed game is
|
||||
later removed by the user and disappears from `app.db`, ShadowMountPlus marks it
|
||||
as deleted in `manual.status` and removes the matching source line from
|
||||
`manual.lst` so it is not installed again automatically. If you later add the
|
||||
same source path back to `manual.lst`, it will be processed again and the status
|
||||
will be updated after installation.
|
||||
|
||||
You can use **Dump Installer** to install or prepare game dumps for this manual
|
||||
workflow, then add the resulting game folder path or image file path to
|
||||
`manual.lst`.
|
||||
|
||||
Recommended folder structure:
|
||||
- Default mode (`scan_depth=1`):
|
||||
- `/data/homebrew/<TITLE_ID>/`
|
||||
- `/data/etaHEN/games/<TITLE_ID>/`
|
||||
- `/data/homebrew/backports/<TITLE_ID>/`
|
||||
- `/data/etaHEN/games/backports/<TITLE_ID>/`
|
||||
|
||||
- Nested mode (`scan_depth=2`):
|
||||
- `/data/homebrew/PS5/<AnyFolder>/<TITLE_ID>/`
|
||||
- `/mnt/ext0/etaHEN/games/<Collection>/<TITLE_ID>/`
|
||||
- `/mnt/ext0/etaHEN/games/backports/<TITLE_ID>/`
|
||||
|
||||
|
||||
## Creating an exFAT image
|
||||
|
||||
Recommended only for titles that need external-drive-style compatibility. For general use, prefer `.ffpkg`.
|
||||
|
||||
Linux (Ubuntu/Debian):
|
||||
- `sudo apt-get install -y exfatprogs exfat-fuse fuse3 rsync`
|
||||
- `truncate -s <image_size> test.exfat`
|
||||
- `mkfs.exfat -c 512 test.exfat`
|
||||
- `mkdir -p /mnt/exfat`
|
||||
- `mount -t exfat-fuse -o loop test.exfat /mnt/exfat`
|
||||
- `rsync -r --info=progress2 APPXXXX/ /mnt/exfat/`
|
||||
- `umount /mnt/exfat`
|
||||
- Required components installation:
|
||||
- `sudo apt-get update && sudo apt-get install -y exfatprogs exfat-fuse fuse3 rsync`
|
||||
- Script: `mkexfat.sh`
|
||||
- Usage: `./mkexfat.sh <game_root_dir> [output_file]`
|
||||
- Example:
|
||||
- `chmod +x mkexfat.sh`
|
||||
- `./mkexfat.sh ./APPXXXX ./PPSA12345.exfat`
|
||||
- Notes:
|
||||
- Source folder must be the game root and contain `eboot.bin`.
|
||||
- Auto-calculates image size using rounded file allocation + metadata + safety margin.
|
||||
- For manual exFAT builds, keep the cluster size at `64 KB` or you may lose performance.
|
||||
- Automatically selects exFAT cluster profile:
|
||||
- Large-file profile: `64K`
|
||||
- Small/mixed-file profile: `32K`
|
||||
|
||||
Windows:
|
||||
- Use ImDisk Toolkit (or an equivalent loop-mount utility).
|
||||
- Recommended: use `make_image.bat` (wrapper for `New-OsfExfatImage.ps1` + OSFMount).
|
||||
- Requirements:
|
||||
- Install OSFMount: https://www.osforensics.com/tools/mount-disk-images.html.
|
||||
- Keep `make_image.bat` and `New-OsfExfatImage.ps1` in the same folder.
|
||||
- Run `cmd.exe` as Administrator.
|
||||
- If you build an exFAT image manually instead of using the script, keep the cluster size at `64 KB` or you may lose performance.
|
||||
- Usage:
|
||||
- `make_image.bat "C:\images\game.exfat" "C:\payload\APPXXXX"`
|
||||
- Behavior:
|
||||
- Auto-sizes the image to fit source content.
|
||||
- Source folder must be the game root and contain `eboot.bin`.
|
||||
- Formats and copies source folder contents into image root.
|
||||
- Optional (fixed size): run PowerShell script directly:
|
||||
- `powershell.exe -ExecutionPolicy Bypass -File .\New-OsfExfatImage.ps1 -ImagePath "C:\images\game.exfat" -SourceDir "C:\payload\APPXXXX" -Size 8G -ForceOverwrite`
|
||||
|
||||
## Why UFS can be problematic
|
||||
## Creating a UFS2 image (`.ffpkg`)
|
||||
|
||||
FreeBSD:
|
||||
- Script: `mkufs2.sh`
|
||||
- Usage: `./mkufs2.sh <game_root_dir> [output_file]`
|
||||
- Example:
|
||||
- `chmod +x mkufs2.sh`
|
||||
- `./mkufs2.sh ./APPXXXX ./PPSA12345.ffpkg`
|
||||
- Notes:
|
||||
- Source folder must be the game root and contain `eboot.bin`.
|
||||
- The script auto-calculates image size using rounded file allocation + metadata + safety margin.
|
||||
- Recommended `newfs` parameters for UFS2:
|
||||
- `newfs -O 2 -b 65536 -f 65536 -m 0 -S 4096`
|
||||
- `mkufs2.sh` keeps this fixed block/fragment/sector profile and auto-tunes `-i` based on source file/directory count.
|
||||
- Rough manual `-i` estimate for manual builds:
|
||||
- `target_inodes ~= file_count + dir_count + 2048`
|
||||
- `bytes_per_inode ~= image_size_bytes / target_inodes`
|
||||
- Round `bytes_per_inode` down to a multiple of `4096`, then keep it in the practical range `65536..262144`.
|
||||
- Practical rule of thumb: use `262144` for normal game dumps, `131072` for tens of thousands of files, and `65536` only for very file-dense images.
|
||||
- Example: for an `8 GiB` image with `60000` files and `4000` directories, `-i ~= 8*1024^3 / (60000 + 4000 + 2048) ~= 130312`, so use `-i 131072`.
|
||||
|
||||
Windows:
|
||||
- You can create UFS2 images with **UFS2Tool** https://github.com/SvenGDK/UFS2Tool.
|
||||
- Example:
|
||||
- `UFS2Tool.exe newfs -O 2 -b 65536 -f 65536 -m 0 -S 4096 -i 262144 -D ./APPXXXX ./PPSA12345.ffpkg`
|
||||
- For manual builds, use `-i 262144` as the baseline and lower it for images with many small files.
|
||||
|
||||
UFS is case-sensitive, while many PKG/game paths expect case-insensitive behavior. Because of that, some titles may fail or behave incorrectly when mounted from UFS images. This is one reason why UFS support is marked experimental.
|
||||
|
||||
## Installation and usage
|
||||
|
||||
@@ -70,39 +399,61 @@ UFS is case-sensitive, while many PKG/game paths expect case-insensitive behavio
|
||||
### Method 1: Manual Payload Injection (Port 9021)
|
||||
Use a payload sender (such as NetCat GUI or a web-based loader) to send the files to **Port 9021**.
|
||||
|
||||
1. Send `notify.elf` (Optional).
|
||||
* *Only send this if you want graphical pop-ups. Skip if you prefer standard notifications.*
|
||||
2. Send `shadowmount.elf`.
|
||||
3. Wait for the notification: *"ShadowMount"*.
|
||||
1. Send `shadowmountplus.elf`.
|
||||
2. Wait for the notification: *"ShadowMount+"*.
|
||||
|
||||
### Method 2: PLK Autoloader (Recommended)
|
||||
Add ShadowMount to your `autoload.txt` for **plk-autoloader** to ensure it starts automatically on every boot.
|
||||
Add ShadowMountPlus to your `autoload.txt` for **plk-autoloader** to ensure it starts automatically on every boot.
|
||||
|
||||
**Sample Configuration:**
|
||||
```ini
|
||||
!1000
|
||||
shadowmountplus.elf
|
||||
!3000
|
||||
kstuff.elf
|
||||
!1000
|
||||
notify.elf ; Optional - Remove this line if you do not want Rich Toasts
|
||||
!1000
|
||||
shadowmount.elf
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If a game is not mounted:
|
||||
- Debug log is enabled by default; if disabled, set `debug=1` in `/data/shadowmount/config.ini`.
|
||||
- Check `/data/shadowmount/debug.log` and system notifications from ShadowMount+.
|
||||
- Verify scan roots:
|
||||
- if `scanpath=...` is set, only these paths are scanned;
|
||||
- `/mnt/shadowmnt/pfsc` and `/mnt/shadowmnt` are always scanned.
|
||||
- Verify scan depth:
|
||||
- `scan_depth=1` scans only first-level subfolders;
|
||||
- `scan_depth=2` scans one additional nested level;
|
||||
- `recursive_scan=1` is treated as deprecated compatibility mode and forces `scan_depth=2`.
|
||||
- If logs show `source not stable yet`, adjust `stability_wait_seconds` (or wait for source copy/write to finish).
|
||||
- Verify game structure:
|
||||
- folder game: `<GAME_DIR>/sce_sys/param.json`;
|
||||
- image game (`.ffpkg` / `.exfat` / `.ffpfs`): `sce_sys/param.json` must be at image root (no extra top-level folder);
|
||||
- PFSC container (`.ffpfsc`): nested supported image files are scanned; direct game files inside the container are ignored.
|
||||
- If you see `missing/invalid param.json` for an image, check via FTP that files are present under `/mnt/shadowmnt/<image_name>_<hash>/` and include `sce_sys/param.json`.
|
||||
- If you see image mount failure, check image integrity and filesystem type (`.ffpkg`=UFS, `.exfat`=exFAT, `.ffpfs`=PFS, `.ffpfsc`=PFS container).
|
||||
- If you see duplicate titleId notification, keep only one source per `<TITLE_ID>`.
|
||||
|
||||
If a game is mounted but does not start:
|
||||
- Check registration notifications (`Register failed ...`).
|
||||
- If the game is not registered, try removing its launcher icon and removing it from Itemzflow.
|
||||
- If this does not help, remove the game data from system settings and retry (this will delete game saves).
|
||||
|
||||
## ⚠️ Notes
|
||||
* **First Run:** If you have a large library, the initial scan may take a few seconds to register all titles.
|
||||
* **Large Games:** For massive games (100GB+), allow a few extra seconds for the system to verify file integrity before the "Installed" notification appears.
|
||||
|
||||
## Credits
|
||||
* **VoidWhisper** - Lead Developer & Logic Implementation
|
||||
* **Drakmor** - Evolution
|
||||
* **Drakmor** - Evolution of ShadowMount to ShadowMountPlus
|
||||
|
||||
* **Special Thanks:**
|
||||
* EchoStretch
|
||||
* VoidWhisper for ShadowMount
|
||||
* BestPig for BackPort
|
||||
* EchoStretch for kstuff-toggle and etc
|
||||
* Gezine
|
||||
* earthonion
|
||||
* LightningMods
|
||||
* john-tornblom
|
||||
* RenanGBarreto for his excellent https://github.com/PSBrew/MkPFS
|
||||
* john-tornblom for SDK
|
||||
* PS5 R&D Community
|
||||
|
||||
---
|
||||
|
||||
[](https://ko-fi.com/voidwhisper)
|
||||
@@ -0,0 +1,158 @@
|
||||
# ShadowMount runtime config example
|
||||
# Place as: /data/shadowmount/config.ini
|
||||
#
|
||||
# This file is a template.
|
||||
# Nothing below is active until you remove the leading '#'.
|
||||
#
|
||||
# How to use this file:
|
||||
# 1. Uncomment only the parameters you really want to override.
|
||||
# 2. Lines that start with '# key=value' are real parameters you can uncomment directly.
|
||||
# 3. Lines that start with '##' are examples/templates only. Edit them before use.
|
||||
|
||||
# Debug logging:
|
||||
# 1/true/yes/on -> enable log_debug output to console and /data/shadowmount/debug.log
|
||||
# 0/false/no/off -> disable log_debug output
|
||||
# Default: 1
|
||||
# debug=1
|
||||
|
||||
# Quiet mode:
|
||||
# 1/true/yes/on -> only show the startup version popup and runtime errors
|
||||
# 0/false/no/off -> also show normal informational popups
|
||||
# Default: 0
|
||||
# quiet_mode=0
|
||||
|
||||
# Global mount mode:
|
||||
# 1/true/yes/on/ro -> read-only mounts
|
||||
# 0/false/no/off/rw -> read-write mounts
|
||||
# Default: 1
|
||||
# mount_read_only=1
|
||||
|
||||
# Mounting even damaged file systems:
|
||||
# 1/true/yes/on -> include force option
|
||||
# 0/false/no/off -> default mount options
|
||||
# Default: 0
|
||||
# force_mount=0
|
||||
|
||||
# Use batch registration for newly staged titles:
|
||||
# 1/true/yes/on -> submit queued installs through sceAppInstUtilAppInstallAll
|
||||
# 0/false/no/off -> register each title directly
|
||||
# Default: 0 on FW below 12.00, forced to 1 on FW 12.00 and newer
|
||||
# app_install_all=0
|
||||
|
||||
# Scan depth relative to each scan root:
|
||||
# 1 -> scan only first-level subdirectories
|
||||
# 2 -> also scan one additional nested level
|
||||
# Default: 1
|
||||
# scan_depth=1
|
||||
|
||||
# Directory watcher granularity:
|
||||
# 1 -> keep vnode watchers on scan root subdirectories
|
||||
# 0 -> watch only scan roots/parents and rely on targeted/full scans for children
|
||||
# Default: 0
|
||||
# watch_subdirs=0
|
||||
|
||||
# Image file watcher granularity:
|
||||
# 1 -> keep vnode watchers on discovered image files
|
||||
# 0 -> detect image file changes through targeted/full scans
|
||||
# Default: 1
|
||||
# watch_image_files=1
|
||||
|
||||
# Legacy compatibility:
|
||||
# recursive_scan=1 forces scan_depth=2
|
||||
# Default: disabled
|
||||
# recursive_scan=0
|
||||
|
||||
# Full scan loop interval (seconds), range: 1..3600
|
||||
# Default: 15
|
||||
# scan_interval_seconds=15
|
||||
|
||||
# Source stabilization wait window (seconds), range: 0..3600
|
||||
# If source mtime/ctime is newer than this window, it will be deferred to next scan.
|
||||
# Default: 10
|
||||
# stability_wait_seconds=10
|
||||
|
||||
# Backend selection per filesystem:
|
||||
# lvd -> /dev/lvdctl -> /dev/lvdN
|
||||
# md -> /dev/mdctl -> /dev/mdN
|
||||
# Defaults: exfat=lvd, ufs=lvd
|
||||
# exfat_backend=lvd
|
||||
# ufs_backend=lvd
|
||||
|
||||
# Sandbox fakelib backport watcher:
|
||||
# 1/true/yes/on -> mount /mnt/sandbox/<TITLE_ID>_XXX/app0/fakelib into common/lib
|
||||
# 0/false/no/off -> disable fakelib watcher
|
||||
# Default: 1
|
||||
# backport_fakelib=1
|
||||
|
||||
# Global fakelib overlay:
|
||||
# 1/true/yes/on -> also mount global fakelib for every tracked game when present
|
||||
# 0/false/no/off -> do not use the global fakelib folder
|
||||
# Default: 1
|
||||
# global_fakelib=1
|
||||
#
|
||||
# Default global fakelib folder:
|
||||
# global_fakelib_path=/data/shadowmount/fakelib
|
||||
#
|
||||
# Priority when both global and per-game fakelib exist:
|
||||
# game -> mount global first, then game-specific fakelib (game wins)
|
||||
# global -> mount game-specific fakelib first, then global fakelib (global wins)
|
||||
# Default: game
|
||||
# global_fakelib_priority=game
|
||||
#
|
||||
## Exclude specific title IDs from the global fakelib overlay (repeatable):
|
||||
## global_fakelib_exclude=PPSA12345
|
||||
|
||||
# Auto-manage kstuff around game launches:
|
||||
# 1/true/yes/on -> pause kstuff after game launch, resume immediately on stop
|
||||
# 0/false/no/off -> leave kstuff state untouched
|
||||
# Default: 1
|
||||
# kstuff_game_auto_toggle=1
|
||||
|
||||
# Detect crash-candidate state and autotune pause delay:
|
||||
# 1/true/yes/on -> enable crash monitoring and autotune updates
|
||||
# 0/false/no/off -> disable crash detection/autotune logic
|
||||
# Default: 1
|
||||
# kstuff_crash_detection=1
|
||||
|
||||
# Delay before pausing kstuff for image-backed titles.
|
||||
# Default: 25
|
||||
# kstuff_pause_delay_image_seconds=25
|
||||
|
||||
# Delay before pausing kstuff for direct/non-image launches.
|
||||
# Default: 15
|
||||
# kstuff_pause_delay_direct_seconds=15
|
||||
|
||||
# Backend-specific sector size overrides:
|
||||
# Defaults: lvd_exfat=512, lvd_ufs=4096, lvd_pfs=32768, md_exfat=512, md_ufs=512
|
||||
# lvd_exfat_sector_size=512
|
||||
# lvd_ufs_sector_size=4096
|
||||
# lvd_pfs_sector_size=32768
|
||||
# md_exfat_sector_size=512
|
||||
# md_ufs_sector_size=512
|
||||
|
||||
# Optional custom scan roots (can be repeated):
|
||||
# If at least one scanpath=... is present, only these paths are used.
|
||||
# "/mnt/shadowmnt/pfsc" and "/mnt/shadowmnt" are always added automatically.
|
||||
# Default: use built-in scan path list
|
||||
# scanpath=/data/homebrew
|
||||
|
||||
## Per-image mount mode override by file name (repeatable):
|
||||
## image_ro=<image_filename>
|
||||
## image_rw=<image_filename>
|
||||
## image_sector=<image_filename>:<sector_size>
|
||||
##
|
||||
## Examples:
|
||||
## image_ro=PPSA12345.ffpfs
|
||||
## image_rw=My Game Dump.exfat
|
||||
## image_sector=My Game Dump.exfat:65536
|
||||
|
||||
## Keep kstuff enabled for specific title IDs (repeatable):
|
||||
## kstuff_no_pause=PPSA12345
|
||||
|
||||
## Override pause delay for specific title IDs (repeatable):
|
||||
## Format: TITLEID:SECONDS
|
||||
## kstuff_delay=PPSA12345:30
|
||||
|
||||
## More custom scan root examples:
|
||||
## scanpath=/data/homebrew
|
||||
## scanpath=/data/etaHEN/games
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef SM_APPDB_H
|
||||
#define SM_APPDB_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
struct AppDbTitleList;
|
||||
|
||||
// Close the cached app.db handle and release related resources.
|
||||
void shutdown_app_db(void);
|
||||
// Normalize existing snd0info rows during process startup.
|
||||
bool app_db_run_startup_maintenance(void);
|
||||
// Update snd0 metadata for a registered title.
|
||||
int update_snd0info(const char *title_id);
|
||||
// Normalize an existing snd0Info path for one title.
|
||||
int normalize_snd0info_for_title(const char *title_id);
|
||||
// Check whether a title ID exists in a cached app.db title list.
|
||||
bool app_db_title_list_contains(const struct AppDbTitleList *list,
|
||||
const char *title_id);
|
||||
// Release a title-list snapshot returned by get_app_db_title_list_cached().
|
||||
void free_app_db_title_list(struct AppDbTitleList *list);
|
||||
// Drop the cached app.db title list so it is reloaded on next access.
|
||||
void invalidate_app_db_title_cache(void);
|
||||
// Copy the cached app.db title list into caller-owned storage, loading it if needed.
|
||||
bool get_app_db_title_list_cached(struct AppDbTitleList *list_out);
|
||||
// Copy the cached PPSA titles that app.db marks as not uninstallable.
|
||||
bool get_app_db_blocked_uninstall_ppsa_list(struct AppDbTitleList *list_out);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,47 @@
|
||||
#ifndef SM_CONFIG_MOUNT_H
|
||||
#define SM_CONFIG_MOUNT_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct runtime_config runtime_config_t;
|
||||
|
||||
// Ensure runtime configuration is loaded before use.
|
||||
void ensure_runtime_config_ready(void);
|
||||
// Load runtime configuration from disk and apply defaults.
|
||||
bool load_runtime_config(void);
|
||||
// Reload runtime configuration from disk when config.ini changed.
|
||||
bool reload_runtime_config_if_changed(bool *reloaded_out);
|
||||
// Return the current runtime configuration.
|
||||
const runtime_config_t *runtime_config(void);
|
||||
// Return the number of configured scan roots.
|
||||
int get_scan_path_count(void);
|
||||
// Return a scan root by index, or NULL if out of range.
|
||||
const char *get_scan_path(int index);
|
||||
// Return scan depth for a root, including managed container-root expansion.
|
||||
uint32_t get_scan_depth_for_root(const char *scan_path);
|
||||
// Resolve a per-image read-only override from the file name.
|
||||
bool get_image_mode_override(const char *filename, bool *mount_read_only_out);
|
||||
// Resolve a per-image sector-size override from autotune.ini or config.ini.
|
||||
bool get_image_sector_size_override(const char *filename,
|
||||
uint32_t *sector_size_out);
|
||||
// Return true when kstuff auto-pause is disabled for the given title ID.
|
||||
bool is_kstuff_pause_disabled_for_title(const char *title_id);
|
||||
// Resolve a per-title kstuff pause-delay override in seconds.
|
||||
bool get_kstuff_pause_delay_override_for_title(const char *title_id,
|
||||
uint32_t *delay_seconds_out);
|
||||
// Resolve a per-title autotuned kstuff pause-delay override in seconds.
|
||||
bool get_kstuff_autotune_pause_delay_for_title(const char *title_id,
|
||||
uint32_t *delay_seconds_out);
|
||||
// Upsert an autotuned pause-delay override for the title.
|
||||
bool upsert_kstuff_autotune_pause_delay(const char *title_id,
|
||||
uint32_t current_delay_seconds,
|
||||
uint32_t *delay_seconds_out);
|
||||
// Upsert an autotuned per-image sector-size override.
|
||||
bool upsert_image_sector_size_autotune(const char *filename,
|
||||
uint32_t sector_size,
|
||||
uint32_t *sector_size_out);
|
||||
// Return true when the global fakelib overlay is disabled for this title.
|
||||
bool is_global_fakelib_excluded_for_title(const char *title_id);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,17 @@
|
||||
#ifndef SM_FAKELIB_H
|
||||
#define SM_FAKELIB_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
// Return true when per-game fakelib backport automation is enabled.
|
||||
bool sm_fakelib_game_feature_enabled(void);
|
||||
// Track a supported game launch and mount its fakelib overlay immediately.
|
||||
void sm_fakelib_game_on_exec(pid_t pid, const char *title_id);
|
||||
// Remove a tracked fakelib overlay for a stopped game.
|
||||
void sm_fakelib_game_on_exit(pid_t pid);
|
||||
// Unmount all tracked overlays during watcher shutdown.
|
||||
void sm_fakelib_game_shutdown(void);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,52 @@
|
||||
#ifndef SM_FILESYSTEM_H
|
||||
#define SM_FILESYSTEM_H
|
||||
|
||||
#include <dirent.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
// Check whether a title is present in the installed app set.
|
||||
bool is_installed(const char *title_id);
|
||||
// Check whether /user/appmeta/<TITLE_ID>/param.json exists.
|
||||
bool has_appmeta_data(const char *title_id);
|
||||
// Check whether a title currently has mounted data.
|
||||
bool is_data_mounted(const char *title_id);
|
||||
// Read the mount.lnk source path for a title tracked under /user/app.
|
||||
bool read_mount_link(const char *title_id, char *out, size_t out_size);
|
||||
// Validate a /user/app/<TITLE_ID> tracker entry and optionally build its path.
|
||||
bool resolve_title_app_dir(const struct dirent *entry, char *app_dir,
|
||||
size_t app_dir_size);
|
||||
// Recover staged mount links and warm image source mappings on startup.
|
||||
void cleanup_staged_mount_links(void);
|
||||
// Remove duplicated managed mount layers left from previous runs.
|
||||
void cleanup_duplicate_title_mounts(void);
|
||||
// Remount /system_ex with the expected flags.
|
||||
int remount_system_ex(void);
|
||||
// Mount a title source into /system_ex/app/<title_id> via nullfs.
|
||||
bool mount_title_nullfs(const char *title_id, const char *src_path);
|
||||
// Roll back a just-mounted title nullfs layer when publication is aborted.
|
||||
bool rollback_title_nullfs_mount(const char *title_id, const char *src_path);
|
||||
// Reconcile the title mount stack against the expected source/backport state.
|
||||
bool reconcile_title_backport_mount(const char *title_id, const char *src_path,
|
||||
const char *expected_backport_path,
|
||||
bool *overlay_active_out);
|
||||
// Mount a prepared backport overlay on top of an already mounted title.
|
||||
// Returns false only when nmount() itself fails.
|
||||
bool mount_backport_overlay(const char *mount_point,
|
||||
const char *backport_path,
|
||||
const char *title_id);
|
||||
// Unmount all managed /system_ex/app/<title_id> mount stacks on shutdown.
|
||||
void shutdown_title_mounts(void);
|
||||
// Remove stale mount links and optionally restore image-backed mounts.
|
||||
void cleanup_mount_links(const char *removed_source_root,
|
||||
bool unmount_system_ex_bind);
|
||||
// Unmount and remove title links that point under a source root.
|
||||
void cleanup_mount_links_for_source_unmount(const char *source_root);
|
||||
// Unmount and remove title links backed by USB sources or USB-backed images.
|
||||
void cleanup_usb_mount_links_for_suspend(void);
|
||||
// Recursively copy a directory tree.
|
||||
int copy_dir(const char *src, const char *dst);
|
||||
// Copy a single file.
|
||||
int copy_file(const char *src, const char *dst);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,26 @@
|
||||
#ifndef SM_GAME_CACHE_H
|
||||
#define SM_GAME_CACHE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef bool (*game_cache_iter_fn)(const char *path, const char *title_id,
|
||||
const char *title_name,
|
||||
const char *owning_scan_root, void *ctx);
|
||||
|
||||
// Cache resolved metadata for a mounted or discovered game.
|
||||
void cache_game_entry(const char *path, const char *title_id,
|
||||
const char *title_name);
|
||||
// Drop invalid or stale entries from the game cache.
|
||||
void prune_game_cache(void);
|
||||
// Drop invalid or stale entries that belong to a specific scan root.
|
||||
void prune_game_cache_for_root(const char *root);
|
||||
// Look up a cached game entry by path or title ID.
|
||||
bool find_cached_game(const char *path, const char *title_id,
|
||||
const char **existing_path_out);
|
||||
// Visit cached games, optionally limited to one source root.
|
||||
void for_each_cached_game_entry(const char *root, game_cache_iter_fn fn,
|
||||
void *ctx);
|
||||
// Remove a game cache entry by path.
|
||||
void clear_cached_game(const char *path);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,17 @@
|
||||
#ifndef SM_GAME_LIFECYCLE_H
|
||||
#define SM_GAME_LIFECYCLE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
// Start the shared game lifecycle watcher used by runtime game features.
|
||||
bool start_game_lifecycle_watcher(void);
|
||||
// Wake the shared watcher so pending lifecycle work is processed immediately.
|
||||
void wake_game_lifecycle_watcher(void);
|
||||
// Stop the shared watcher and let lifecycle modules clean up tracked state.
|
||||
void stop_game_lifecycle_watcher(void);
|
||||
// Return true when a supported game is currently being tracked as running.
|
||||
bool sm_game_lifecycle_has_active_game(void);
|
||||
// Ensure the shared watcher is running for runtime game-state tracking.
|
||||
bool refresh_game_lifecycle_watcher(void);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,13 @@
|
||||
#ifndef SM_GAMEINFO_H
|
||||
#define SM_GAMEINFO_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
// Read title ID and title name from a mounted game directory.
|
||||
bool get_game_info(const char *base_path, const struct stat *param_st,
|
||||
char *out_id, char *out_name);
|
||||
// Check whether a directory contains sce_sys/param.json and stat it.
|
||||
bool directory_has_param_json(const char *dir_path, struct stat *param_st_out);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,16 @@
|
||||
#ifndef SM_HASH_H
|
||||
#define SM_HASH_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// Return a stable 32-bit FNV-1a hash for a NUL-terminated string.
|
||||
static inline uint32_t sm_fnv1a32(const char *s) {
|
||||
uint32_t h = 2166136261u;
|
||||
while (s && *s) {
|
||||
h ^= (uint8_t)(*s++);
|
||||
h *= 16777619u;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,35 @@
|
||||
#ifndef SM_IMAGE_H
|
||||
#define SM_IMAGE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "sm_types.h"
|
||||
|
||||
// Log filesystem statistics for a mounted path.
|
||||
void log_fs_stats(const char *tag, const char *path, const char *type_hint);
|
||||
// Attach and mount an image file to its runtime mount point.
|
||||
bool mount_image(const char *file_path, image_fs_type_t fs_type);
|
||||
// Unmount an image mount point and detach its backing device.
|
||||
bool unmount_image(const char *file_path, int unit_id, attach_backend_t backend);
|
||||
// Reconcile cached image mounts with current sources and remount if needed.
|
||||
void cleanup_stale_image_mounts(void);
|
||||
// Reconcile cached image mounts that belong to a specific scan root.
|
||||
void cleanup_stale_image_mounts_for_root(const char *root);
|
||||
// Unmount cached image mounts backed by USB storage during suspend.
|
||||
bool unmount_usb_image_mounts_for_suspend(void);
|
||||
// Unmount every cached image mount during shutdown.
|
||||
bool shutdown_image_mounts(void);
|
||||
// Remove empty directories left under the image mount root.
|
||||
void cleanup_mount_dirs(void);
|
||||
// Mount an image file if it is stable and not currently rate-limited.
|
||||
bool maybe_mount_image_file(const char *full_path, const char *name,
|
||||
bool *unstable_out);
|
||||
// Build the deterministic runtime mount point for an image source path.
|
||||
void get_image_mount_point_for_source(const char *file_path,
|
||||
char mount_point[MAX_PATH]);
|
||||
// Return true when the filename has a supported image extension.
|
||||
bool is_supported_image_file_name(const char *name);
|
||||
// Return true when the source path is a supported image, including nested names.
|
||||
bool is_supported_image_file_path(const char *full_path, const char *name);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,34 @@
|
||||
#ifndef SM_IMAGE_CACHE_H
|
||||
#define SM_IMAGE_CACHE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "sm_types.h"
|
||||
|
||||
typedef struct {
|
||||
char path[MAX_PATH];
|
||||
char mount_point[MAX_PATH];
|
||||
int unit_id;
|
||||
attach_backend_t backend;
|
||||
} image_cache_entry_t;
|
||||
|
||||
// Cache a successful image mount and its attached device.
|
||||
// Returns false when no free cache slot is available.
|
||||
bool cache_image_mount(const char *path, const char *mount_point,
|
||||
int unit_id, attach_backend_t backend);
|
||||
// Cache an image source mapping without attach metadata.
|
||||
bool cache_image_source_mapping(const char *path, const char *mount_point);
|
||||
// Return a cached image mount entry by index.
|
||||
bool get_image_cache_entry(int index, image_cache_entry_t *entry_out);
|
||||
// Mark a cached image mount entry as invalid.
|
||||
void invalidate_image_cache_entry(int index);
|
||||
// Resolve a device mapping from the in-memory mount cache.
|
||||
bool resolve_device_from_mount_cache(const char *mount_point,
|
||||
attach_backend_t *backend_out,
|
||||
int *unit_out);
|
||||
// Resolve the original image file path for a cached runtime mount point.
|
||||
bool resolve_image_source_from_mount_cache(const char *mount_point,
|
||||
char *path_out,
|
||||
size_t path_out_size);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,10 @@
|
||||
#ifndef SM_INSTALL_H
|
||||
#define SM_INSTALL_H
|
||||
|
||||
typedef struct scan_candidate scan_candidate_t;
|
||||
|
||||
// Install or remount the collected scan candidates.
|
||||
void process_scan_candidates(const scan_candidate_t *candidates,
|
||||
int candidate_count);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef SM_INSTALL_QUEUE_H
|
||||
#define SM_INSTALL_QUEUE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct scan_candidate scan_candidate_t;
|
||||
|
||||
// Return whether a title is already waiting for async batch installation.
|
||||
bool is_title_install_pending(const char *title_id);
|
||||
// Drop queued/submitted async install state for a title after confirmed removal.
|
||||
void sm_install_forget_pending_title(const char *title_id);
|
||||
// Return the next pending-install wake deadline in monotonic microseconds.
|
||||
uint64_t sm_install_next_wake_us(uint64_t now_us);
|
||||
// Poll async batch installs for success/timeout completion only.
|
||||
void sm_install_poll_pending(void);
|
||||
// Poll async batch installs for success/timeout completion and auto-submit
|
||||
// queued installs when possible.
|
||||
void sm_install_service_pending(void);
|
||||
// Queue a staged title for the next batch install request.
|
||||
bool sm_install_queue_candidate(const scan_candidate_t *candidate,
|
||||
bool has_src_snd0);
|
||||
// Submit all queued titles when no batch install is currently pending.
|
||||
bool sm_install_submit_queued(void);
|
||||
// Record retry state for every queued title after a batch submit failure.
|
||||
void sm_install_note_submit_failure(void);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,41 @@
|
||||
#ifndef SM_KSTUFF_H
|
||||
#define SM_KSTUFF_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
// Resolve firmware-specific kstuff sysentvec addresses and initialize state.
|
||||
void sm_kstuff_init(void);
|
||||
// Shut down runtime kstuff control state.
|
||||
void sm_kstuff_shutdown(void);
|
||||
// Return true when firmware-specific kstuff control is available.
|
||||
bool sm_kstuff_is_supported(void);
|
||||
// Return true when both tracked kstuff sysentvecs are enabled.
|
||||
bool sm_kstuff_is_enabled(void);
|
||||
// Set both tracked kstuff sysentvecs and return the resulting enabled state.
|
||||
bool sm_kstuff_set_enabled(bool enabled, bool notify_user);
|
||||
|
||||
// Return true when game-driven kstuff automation should run.
|
||||
bool sm_kstuff_game_feature_enabled(void);
|
||||
// Track a supported game launch for delayed auto-pause handling.
|
||||
void sm_kstuff_game_on_exec(pid_t pid, const char *title_id, uint32_t app_id,
|
||||
uint64_t exec_time_us);
|
||||
// Publish an AppFocus change for processing on the lifecycle/kstuff thread.
|
||||
void sm_kstuff_note_app_focus(uint32_t app_id);
|
||||
// Return the next wake deadline in monotonic microseconds, or 0 when idle.
|
||||
uint64_t sm_kstuff_game_next_wake_us(uint64_t now_us);
|
||||
// Forget a tracked game and restore kstuff when no paused entries remain.
|
||||
void sm_kstuff_game_on_exit(pid_t pid);
|
||||
// Apply any delayed kstuff pause whose deadline has elapsed.
|
||||
void sm_kstuff_game_poll(void);
|
||||
// Clear tracked game state and restore kstuff if the watcher paused it.
|
||||
void sm_kstuff_game_shutdown(void);
|
||||
// Pause kstuff for runtime sleep without restoring game-driven pauses.
|
||||
void sm_kstuff_sleep_enter(void);
|
||||
// Restore kstuff after runtime sleep when sleep entry disabled it.
|
||||
void sm_kstuff_sleep_leave(void);
|
||||
// Re-apply runtime config changes to the currently tracked game, if any.
|
||||
void sm_kstuff_on_config_reload(void);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,50 @@
|
||||
#ifndef SM_LIMITS_H
|
||||
#define SM_LIMITS_H
|
||||
|
||||
#define DEFAULT_SCAN_INTERVAL_US 15000000u
|
||||
#define DEFAULT_SCAN_DEPTH 1u
|
||||
#define DEFAULT_STABILITY_WAIT_SECONDS 10u
|
||||
#define DEFAULT_KSTUFF_PAUSE_DELAY_IMAGE_SECONDS 25u
|
||||
#define DEFAULT_KSTUFF_PAUSE_DELAY_DIRECT_SECONDS 15u
|
||||
|
||||
#define MAX_PENDING 512
|
||||
#define MAX_IMAGE_MOUNTS 256
|
||||
#define MAX_IMAGE_MODE_RULES 128
|
||||
#define MAX_KSTUFF_TITLE_RULES 128
|
||||
#define MAX_FAKELIB_EXCLUDE_RULES 128
|
||||
#define PATH_STATE_CAPACITY MAX_PENDING
|
||||
#define TITLE_STATE_CAPACITY MAX_PENDING
|
||||
#define STATE_HASH_SIZE 1024u
|
||||
#define MAX_SCAN_PATHS 256
|
||||
|
||||
#define MAX_FAILED_MOUNT_ATTEMPTS 2
|
||||
#define MAX_REGISTER_ATTEMPTS 2
|
||||
#define MAX_MISSING_PARAM_SCAN_ATTEMPTS 3
|
||||
#define MAX_IMAGE_MOUNT_ATTEMPTS 3
|
||||
#define MAX_LAYERED_UNMOUNT_ATTEMPTS 4
|
||||
|
||||
#define LVD_RELEASE_WAIT_MAX_US 15000000u
|
||||
#define LVD_RELEASE_WAIT_POLL_US 500000u
|
||||
#define GAME_LIFECYCLE_POLL_INTERVAL_US 250000u
|
||||
#define GAME_APPINFO_LOOKUP_TIMEOUT_US 1000000u
|
||||
#define KSTUFF_RESTORE_RETRY_US 1000000u
|
||||
#define SHELLCORE_FLAG_POLL_INTERVAL_US 100000u
|
||||
#define MIN_SCAN_DEPTH 1u
|
||||
#define MAX_SCAN_DEPTH 2u
|
||||
#define MIN_SCAN_INTERVAL_SECONDS 1u
|
||||
#define MAX_SCAN_INTERVAL_SECONDS 3600u
|
||||
#define MAX_STABILITY_WAIT_SECONDS 3600u
|
||||
#define MAX_KSTUFF_PAUSE_DELAY_SECONDS 3600u
|
||||
|
||||
#define APP_DB_QUERY_BUSY_RETRIES 3
|
||||
#define APP_DB_UPDATE_BUSY_RETRIES 25
|
||||
#define APP_DB_PREPARE_BUSY_RETRIES 25
|
||||
#define APP_DB_BUSY_RETRY_SLEEP_US 200000u
|
||||
#define APP_DB_BUSY_TIMEOUT_MS 5000
|
||||
|
||||
#define MAX_PATH 1024
|
||||
#define MAX_TITLE_ID 32
|
||||
#define MAX_TITLE_NAME 256
|
||||
#define MAX_PARAM_JSON_SIZE (1024u * 1024u)
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,36 @@
|
||||
#ifndef SM_LOG_H
|
||||
#define SM_LOG_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef struct sm_error sm_error_t;
|
||||
|
||||
// Write a formatted line to the debug log.
|
||||
void log_debug(const char *fmt, ...);
|
||||
// Prepare notification assets such as the packaged icon file.
|
||||
void sm_notifications_init(void);
|
||||
// Flush and close persistent log resources.
|
||||
void sm_log_shutdown(void);
|
||||
// Send a rich toast notification with packaged icon and version header.
|
||||
void notify_system_rich(bool allow_in_quiet_mode, const char *fmt, ...);
|
||||
// Send the "game installed" rich toast for the given title ID.
|
||||
void notify_game_installed_rich(const char *title_id);
|
||||
// Send an informational system notification (suppressed in quiet mode).
|
||||
void notify_system_info(const char *fmt, ...);
|
||||
// Send a system notification with formatted text.
|
||||
void notify_system(const char *fmt, ...);
|
||||
// Clear the last recorded subsystem error.
|
||||
void sm_error_clear(void);
|
||||
// Store the last subsystem error with formatted details.
|
||||
void sm_error_set(const char *subsystem, int code, const char *path,
|
||||
const char *fmt, ...);
|
||||
// Return the last recorded subsystem error.
|
||||
const sm_error_t *sm_last_error(void);
|
||||
// Return whether the current error was already notified.
|
||||
bool sm_error_notified(void);
|
||||
// Mark the current error as already shown to the user.
|
||||
void sm_error_mark_notified(void);
|
||||
// Notify the user about an image mount failure once per error state.
|
||||
void notify_image_mount_failed(const char *path, int mount_err);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,20 @@
|
||||
#ifndef SM_MANUAL_H
|
||||
#define SM_MANUAL_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
struct AppDbTitleList;
|
||||
|
||||
// Visit each manual source path from manual.lst.
|
||||
bool sm_manual_for_each_path(bool (*visit)(const char *path, void *ctx),
|
||||
void *ctx);
|
||||
// Mark previously installed manual titles as deleted when they disappear from
|
||||
// app.db, and remove their source lines from manual.lst.
|
||||
bool sm_manual_reconcile_deleted_titles(const struct AppDbTitleList *app_db_titles,
|
||||
bool app_db_titles_ready);
|
||||
// Record that a manual source is installed and associated with a title.
|
||||
void sm_manual_note_installed(const char *manual_source_path,
|
||||
const char *title_id,
|
||||
const char *title_name);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,26 @@
|
||||
#ifndef SM_MDBG_H
|
||||
#define SM_MDBG_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
// Initialize mdbg-backed crash monitoring state.
|
||||
void sm_mdbg_init(void);
|
||||
// Shut down mdbg-backed crash monitoring state.
|
||||
void sm_mdbg_shutdown(void);
|
||||
// Start monitoring the tracked game process for crash-candidate state.
|
||||
void sm_mdbg_game_on_exec(pid_t pid, const char *title_id, uint32_t app_id);
|
||||
// Record that kstuff auto-pause was actually applied to the tracked game.
|
||||
void sm_mdbg_game_on_kstuff_pause(pid_t pid, uint64_t pause_time_us,
|
||||
uint32_t pause_delay_seconds);
|
||||
// Stop monitoring the tracked game process on exit.
|
||||
void sm_mdbg_game_on_exit(pid_t pid);
|
||||
// Clear tracked game monitoring state unconditionally.
|
||||
void sm_mdbg_game_shutdown(void);
|
||||
// Return the next wake deadline in monotonic microseconds, or 0 when idle.
|
||||
uint64_t sm_mdbg_next_wake_us(uint64_t now_us);
|
||||
// Poll the tracked process for mdbg crash-candidate state.
|
||||
void sm_mdbg_poll(void);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,83 @@
|
||||
#ifndef SM_MOUNT_DEFS_H
|
||||
#define SM_MOUNT_DEFS_H
|
||||
|
||||
#define IMAGE_MOUNT_READ_ONLY 1
|
||||
|
||||
// 1 = use legacy /dev/mdctl backend for .exfat images, 0 = use LVD for all.
|
||||
#define EXFAT_ATTACH_USE_MDCTL 0
|
||||
// 1 = allow mounting .ffpkg images via /dev/mdctl, 0 = keep UFS on LVD.
|
||||
#define UFS_ATTACH_USE_MDCTL 0
|
||||
|
||||
// --- LVD Definitions ---
|
||||
// Kernel exposes base attach (V0), extended attach (V1/Attach2) and detach.
|
||||
// This code only builds V0 layer descriptors, so it uses the base attach path.
|
||||
// Wrapper-side raw attach presets:
|
||||
// - 0x8/0x9 -> single-image/save-data family (normalized 0x14/0x1C)
|
||||
// - 0xC/0xD -> download-data/LWFS family (normalized 0x16/0x1E)
|
||||
// raw bit3 (0x8) sets the DONT_MAP_BM-style mode, raw bit2 (0x4) switches to
|
||||
// the DD/LWFS family, and raw bit0 (0x1) selects the alternate preset inside a
|
||||
// family. Firmware pairs bit0 with read-only mount variants, but this is not a
|
||||
// standalone LVD readonly bit by itself.
|
||||
// image_type values accepted by validator: 0..0xC (13 values total).
|
||||
// layer source_type observed: 1=file, 2=device/special source (/dev/sbram0, char/block).
|
||||
// layer descriptor flag bit0 is "no bitmap file specified".
|
||||
#define LVD_CTRL_PATH "/dev/lvdctl"
|
||||
#define MD_CTRL_PATH "/dev/mdctl"
|
||||
#define SCE_LVD_IOC_ATTACH_V0 0xC0286D00
|
||||
#define SCE_LVD_IOC_ATTACH_V1 0xC0286D09
|
||||
#define SCE_LVD_IOC_DETACH 0xC0286D01
|
||||
#define LVD_ATTACH_IO_VERSION_V0 0u
|
||||
#define LVD_ATTACH_IO_VERSION_V1 1u
|
||||
#define LVD_ATTACH_RAW_FLAGS_SINGLE_RO 0x9
|
||||
#define LVD_ATTACH_RAW_FLAGS_SINGLE_RW 0x8
|
||||
#define LVD_ATTACH_RAW_FLAGS_DD_RO 0xD
|
||||
#define LVD_ATTACH_RAW_FLAGS_DD_RW 0xC
|
||||
#
|
||||
#define LVD_SECTOR_SIZE_EXFAT 512u
|
||||
#define LVD_SECTOR_SIZE_UFS 4096u
|
||||
#define LVD_SECTOR_SIZE_PFS 4096u
|
||||
#define LVD_SECONDARY_UNIT_SINGLE_IMAGE 0x10000u
|
||||
#define MD_SECTOR_SIZE_EXFAT 512u
|
||||
#define MD_SECTOR_SIZE_UFS 512u
|
||||
// Raw option bits are normalized by sceFsLvdAttachCommon before validation:
|
||||
// raw:0x1->norm:0x08, raw:0x2->norm:0x80, raw:0x4->norm:0x02, raw:0x8->norm:0x10.
|
||||
// The normalized masks are then checked against validator constraints (0x82/0x92).
|
||||
#define LVD_ATTACH_IMAGE_TYPE_SINGLE 0
|
||||
#define LVD_ATTACH_IMAGE_TYPE_UFS_DOWNLOAD_DATA 7
|
||||
#define LVD_ATTACH_IMAGE_TYPE_PFS_SAVE_DATA 5
|
||||
#define PFS_NESTED_OUTER_IMG_TYPE 0x02u
|
||||
#define PFS_NESTED_INNER_IMG_TYPE 0x82u
|
||||
#define LVD_ATTACH_LAYER_COUNT 1
|
||||
#define LVD_ATTACH_LAYER_ARRAY_SIZE 3
|
||||
#define LVD_ENTRY_TYPE_FILE 1
|
||||
#define LVD_ENTRY_TYPE_SPECIAL 2
|
||||
#define LVD_ENTRY_FLAG_NO_BITMAP 0x1
|
||||
#define LVD_NODE_WAIT_US 100000
|
||||
#define LVD_NODE_WAIT_RETRIES 100
|
||||
#define UFS_NMOUNT_FLAG_RW 0x10000000u
|
||||
#define UFS_NMOUNT_FLAG_RO 0x10000001u
|
||||
|
||||
// --- devpfs/pfs option defaults ---
|
||||
// PFS nmount key/value variants observed in refs:
|
||||
// - fstype: "pfs", "transaction_pfs", "ppr_pfs"
|
||||
// - mkeymode: "SD"
|
||||
// - budgetid: "game"/"system" in init paths
|
||||
// - sigverify/playgo/disc: "0" or "1"
|
||||
// - optional keys in specific flows: ekpfs/eekpfs, eekc, pubkey_ver, key_ver,
|
||||
// finalized, ppkg_opt, sblock_offset, maxpkgszingib
|
||||
#define DEVPFS_BUDGET_GAME "game"
|
||||
#define DEVPFS_BUDGET_SYSTEM "system"
|
||||
#define DEVPFS_MKEYMODE_SD "SD"
|
||||
#define DEVPFS_MKEYMODE_GD "GD"
|
||||
#define DEVPFS_MKEYMODE_AC "AC"
|
||||
#define PFS_MOUNT_BUDGET_ID DEVPFS_BUDGET_GAME
|
||||
#define PFS_MOUNT_MKEYMODE DEVPFS_MKEYMODE_AC
|
||||
#define PFS_MOUNT_SIGVERIFY 0
|
||||
#define PFS_MOUNT_PLAYGO 0
|
||||
#define PFS_MOUNT_DISC 0
|
||||
#define PFS_GDDR5_CACHE_IOCTL 0x80209168
|
||||
// 4x64-bit PFS key encoded as 64 hex chars.
|
||||
#define PFS_ZERO_EKPFS_KEY_HEX \
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,25 @@
|
||||
#ifndef SM_MOUNT_DEVICE_H
|
||||
#define SM_MOUNT_DEVICE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "sm_types.h"
|
||||
|
||||
// Return a printable name for an attach backend enum value.
|
||||
const char *attach_backend_name(attach_backend_t backend);
|
||||
// Check whether a source path has aged enough to be mounted safely.
|
||||
bool is_source_stable_for_mount(const char *path, const char *name,
|
||||
const char *tag);
|
||||
// Wait until a device node appears or disappears.
|
||||
bool wait_for_dev_node_state(const char *devname, bool should_exist);
|
||||
// Resolve backend and unit ID for a mounted image path.
|
||||
bool resolve_device_from_mount(const char *mount_point,
|
||||
attach_backend_t *backend_out, int *unit_out);
|
||||
// Return true if a path is an active mount point.
|
||||
bool is_active_image_mount_point(const char *path);
|
||||
// Wait until /dev/lvd2 is no longer mounted during startup.
|
||||
bool wait_for_lvd_release(void);
|
||||
// Detach a previously attached MD or LVD unit.
|
||||
bool detach_attached_unit(attach_backend_t backend, int unit_id);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,42 @@
|
||||
#ifndef SM_PATH_STATE_H
|
||||
#define SM_PATH_STATE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
// Load cached game info if the param file state still matches.
|
||||
bool load_cached_game_info(const char *path, const struct stat *param_st,
|
||||
char *out_id, char *out_name, bool *valid_out);
|
||||
// Store parsed game info together with the current param file state.
|
||||
void store_cached_game_info(const char *path, const struct stat *param_st,
|
||||
bool valid, const char *title_id,
|
||||
const char *title_name);
|
||||
// Drop expired entries from path-based retry and metadata state.
|
||||
void prune_path_state(void);
|
||||
// Drop expired entries that belong to a specific scan root.
|
||||
void prune_path_state_for_root(const char *root);
|
||||
// Return whether scans for a path are temporarily suppressed after failures.
|
||||
bool is_missing_param_scan_limited(const char *path);
|
||||
// Record a missing param.json failure for scan backoff.
|
||||
void record_missing_param_failure(const char *path);
|
||||
// Clear missing-param backoff state for a path.
|
||||
void clear_missing_param_entry(const char *path);
|
||||
// Return whether image mount retries are temporarily suppressed for a path.
|
||||
bool is_image_mount_limited(const char *path);
|
||||
// Increment and return the image mount retry counter for a path.
|
||||
uint8_t bump_image_mount_attempts(const char *path);
|
||||
// Clear image mount retry state for a path.
|
||||
void clear_image_mount_attempts(const char *path);
|
||||
// Return whether backport overlay remounts are suppressed for a path.
|
||||
bool is_backport_mount_blocked(const char *path);
|
||||
// Record the first failed backport overlay mount for a path.
|
||||
void note_backport_mount_failure(const char *path);
|
||||
// Clear failed backport overlay mount state for a path.
|
||||
void clear_backport_mount_failure(const char *path);
|
||||
// Return true once for a missing manual source until the path appears again.
|
||||
bool note_manual_missing_source_once(const char *path);
|
||||
// Allow a manual source to be reported missing again after it becomes visible.
|
||||
void clear_manual_missing_source(const char *path);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,24 @@
|
||||
#ifndef SM_PATH_UTILS_H
|
||||
#define SM_PATH_UTILS_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "sm_types.h"
|
||||
|
||||
// Return the last path component without modifying the input string.
|
||||
const char *get_filename_component(const char *path);
|
||||
// Return true when stat() can see the path.
|
||||
bool path_exists(const char *path);
|
||||
// Return true when a path lives under the image mount root.
|
||||
bool is_under_image_mount_base(const char *path);
|
||||
// Return true when a path is the PFSC container mount root or lives under it.
|
||||
bool is_pfsc_image_mount_base_or_child(const char *path);
|
||||
// Return true when path is equal to root or is under it.
|
||||
bool path_matches_root_or_child(const char *path, const char *root);
|
||||
// Return true when a path lives on USB-backed storage.
|
||||
bool is_usb_storage_path(const char *path);
|
||||
// Build "<scan_path>/backports" for a managed scan root.
|
||||
bool build_backports_root_path(const char *scan_path, char out[MAX_PATH]);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,47 @@
|
||||
#ifndef SM_PATHS_H
|
||||
#define SM_PATHS_H
|
||||
|
||||
#define IMAGE_MOUNT_BASE "/mnt/shadowmnt"
|
||||
#define PFSC_IMAGE_MOUNT_BASE "/mnt/shadowmnt/pfsc"
|
||||
|
||||
#define DEFAULT_BACKPORTS_DIR_NAME "backports"
|
||||
#define DEFAULT_GLOBAL_FAKELIB_PATH "/data/shadowmount/fakelib"
|
||||
#define LOG_DIR "/data/shadowmount"
|
||||
#define LOG_FILE "/data/shadowmount/debug.log"
|
||||
#define LOG_FILE_PREV "/data/shadowmount/debug.log.1"
|
||||
#define CONFIG_FILE "/data/shadowmount/config.ini"
|
||||
#define AUTOTUNE_FILE "/data/shadowmount/autotune.ini"
|
||||
#define MANUAL_LIST_FILE "/data/shadowmount/manual.lst"
|
||||
#define MANUAL_STATUS_FILE "/data/shadowmount/manual.status"
|
||||
#define APPMETA_BASE "/user/appmeta"
|
||||
#define APP_BASE "/user/app"
|
||||
#define KSTUFF_NOAUTOMOUNT_FILE "/data/.kstuff_noautomount"
|
||||
#define KILL_FILE "/data/shadowmount/STOP"
|
||||
#define TOAST_FILE "/data/shadowmount/notify.txt"
|
||||
#define NOTIFY_ICON_DIR "/user/data/shadowmount"
|
||||
#define NOTIFY_ICON_FILE "/user/data/shadowmount/smp_icon.png"
|
||||
#define APP_DB_PATH "/system_data/priv/mms/app.db"
|
||||
|
||||
/* Compile-time default scan roots (used when config has no scanpath entries). */
|
||||
#define SM_DEFAULT_SCAN_PATHS_INITIALIZER \
|
||||
{ \
|
||||
/* Internal */ \
|
||||
"/data/homebrew", "/data/etaHEN/games", \
|
||||
/* Extended Storage */ \
|
||||
"/mnt/ext0/homebrew", "/mnt/ext0/etaHEN/games", \
|
||||
/* M.2 Drive */ \
|
||||
"/mnt/ext1/homebrew", "/mnt/ext1/etaHEN/games", \
|
||||
/* USB Subfolders */ \
|
||||
"/mnt/usb0/homebrew", "/mnt/usb1/homebrew", "/mnt/usb2/homebrew", \
|
||||
"/mnt/usb3/homebrew", "/mnt/usb4/homebrew", "/mnt/usb5/homebrew", \
|
||||
"/mnt/usb6/homebrew", "/mnt/usb7/homebrew", \
|
||||
"/mnt/usb0/etaHEN/games", "/mnt/usb1/etaHEN/games", \
|
||||
"/mnt/usb2/etaHEN/games", "/mnt/usb3/etaHEN/games", \
|
||||
"/mnt/usb4/etaHEN/games", "/mnt/usb5/etaHEN/games", \
|
||||
"/mnt/usb6/etaHEN/games", "/mnt/usb7/etaHEN/games", \
|
||||
/* USB Root Paths */ \
|
||||
"/mnt/usb0", "/mnt/usb1", "/mnt/usb2", "/mnt/usb3", "/mnt/usb4", \
|
||||
"/mnt/usb5", "/mnt/usb6", "/mnt/usb7", "/mnt/ext0", "/mnt/ext1", NULL \
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,65 @@
|
||||
#ifndef SM_PLATFORM_H
|
||||
#define SM_PLATFORM_H
|
||||
|
||||
#include <ctype.h>
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <signal.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/file.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/mdioctl.h>
|
||||
#include <sys/mount.h>
|
||||
#include <sys/param.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/uio.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <ps5/kernel.h>
|
||||
|
||||
typedef struct {
|
||||
uint32_t app_id;
|
||||
uint64_t unknown1;
|
||||
char title_id[14];
|
||||
char unknown2[0x3c];
|
||||
} app_info_t;
|
||||
|
||||
static inline uint32_t sm_firmware_major_version(void) {
|
||||
uint32_t fw = kernel_get_fw_version();
|
||||
uint32_t major_bcd = (fw >> 24) & 0xFFu;
|
||||
return ((major_bcd >> 4) & 0xFu) * 10u + (major_bcd & 0xFu);
|
||||
}
|
||||
|
||||
// --- SDK Imports ---
|
||||
int sceAppInstUtilInitialize(void);
|
||||
int sceAppInstUtilAppInstallAll(void);
|
||||
int sceAppInstUtilAppUnInstall(const char *title_id);
|
||||
int sceKernelGetAppInfo(pid_t pid, app_info_t *info);
|
||||
int sceKernelUsleep(unsigned int microseconds);
|
||||
int sceUserServiceInitialize(void *);
|
||||
void sceUserServiceTerminate(void);
|
||||
|
||||
// Standard Notification
|
||||
typedef struct notify_request {
|
||||
char unused[45];
|
||||
char message[3075];
|
||||
} notify_request_t;
|
||||
int sceKernelSendNotificationRequest(int, notify_request_t *, size_t, int);
|
||||
|
||||
#define IOVEC_ENTRY(x) \
|
||||
{(void *)(x), \
|
||||
((const char *)(x) == NULL ? 0u : (size_t)(strlen((const char *)(x)) + 1u))}
|
||||
#define IOVEC_SIZE(x) (sizeof(x) / sizeof(struct iovec))
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,31 @@
|
||||
#ifndef SM_RUNTIME_H
|
||||
#define SM_RUNTIME_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
// Install process signal handlers used for graceful shutdown.
|
||||
void install_signal_handlers(void);
|
||||
// Return process pid with the given name, or 0 if not found.
|
||||
pid_t find_pid_by_name(const char *name, bool exclude_self);
|
||||
// Return true when shutdown was requested by signal or kill file.
|
||||
bool should_stop_requested(void);
|
||||
// Request graceful shutdown with a descriptive source string.
|
||||
void request_shutdown_stop(const char *reason);
|
||||
// Return true when the payload is paused for system suspend/resume.
|
||||
bool runtime_sleep_mode_active(void);
|
||||
// Enter or leave sleep mode with a descriptive source string.
|
||||
// Returns true when the runtime sleep state changed.
|
||||
bool request_runtime_sleep_mode(bool active, const char *reason);
|
||||
// Serialize mount/link/cache publication against suspend cleanup.
|
||||
void runtime_mount_state_lock(void);
|
||||
void runtime_mount_state_unlock(void);
|
||||
// Request an immediate scan cycle with a descriptive source string.
|
||||
void request_scan_now(const char *reason);
|
||||
// Consume a pending immediate scan request and copy its reason into caller storage.
|
||||
bool consume_scan_now_request(char *reason_out, size_t reason_out_size);
|
||||
// Sleep in chunks and stop early if shutdown was requested.
|
||||
bool sleep_with_stop_check(unsigned int total_us);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,27 @@
|
||||
#ifndef SM_SCAN_H
|
||||
#define SM_SCAN_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef struct scan_candidate scan_candidate_t;
|
||||
|
||||
// Unmount and clean up mounts whose backing sources disappeared.
|
||||
void cleanup_lost_sources_before_scan(void);
|
||||
// Unmount and clean up mounts whose backing sources disappeared under one root.
|
||||
void cleanup_lost_sources_for_scan_root(const char *scan_root);
|
||||
// Immediately unmount runtime mounts backed by USB storage for suspend.
|
||||
void unmount_usb_sources_for_suspend(void);
|
||||
// Scan configured roots and collect install candidates.
|
||||
int collect_scan_candidates(scan_candidate_t *candidates, int max_candidates,
|
||||
int *total_found_out,
|
||||
bool *unstable_found_out);
|
||||
// Scan a single configured root and collect install candidates.
|
||||
int collect_scan_candidates_for_scan_root(const char *scan_root,
|
||||
scan_candidate_t *candidates,
|
||||
int max_candidates,
|
||||
int *total_found_out,
|
||||
bool *unstable_found_out);
|
||||
// Mount stable backport overlays for already mounted titles.
|
||||
void mount_backport_overlays(bool *unstable_found_out);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,31 @@
|
||||
#ifndef SM_SCAN_TREE_H
|
||||
#define SM_SCAN_TREE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef enum {
|
||||
SM_SCAN_TREE_DIR_DESCEND = 0,
|
||||
SM_SCAN_TREE_DIR_SKIP_DESCEND,
|
||||
SM_SCAN_TREE_DIR_ABORT,
|
||||
} sm_scan_tree_dir_visit_t;
|
||||
|
||||
typedef sm_scan_tree_dir_visit_t (*sm_scan_tree_dir_fn)(const char *dir_path,
|
||||
unsigned int depth_from_root,
|
||||
void *ctx);
|
||||
typedef bool (*sm_scan_tree_image_fn)(const char *image_path,
|
||||
const char *image_name,
|
||||
unsigned int depth_from_root,
|
||||
void *ctx);
|
||||
|
||||
typedef struct {
|
||||
sm_scan_tree_dir_fn on_directory;
|
||||
sm_scan_tree_image_fn on_image_file;
|
||||
} sm_scan_tree_callbacks_t;
|
||||
|
||||
// Walk a managed scan subtree using the shared scan-depth and path filtering rules.
|
||||
bool sm_scan_tree_walk(const char *scan_root, const char *dir_path,
|
||||
unsigned int depth_from_root,
|
||||
unsigned int remaining_depth,
|
||||
const sm_scan_tree_callbacks_t *callbacks, void *ctx);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,17 @@
|
||||
#ifndef SM_SCANNER_H
|
||||
#define SM_SCANNER_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
// Initialize scanner service resources such as wake pipe and file watches.
|
||||
bool sm_scanner_init(void);
|
||||
// Wake a blocked scanner wait. Safe to call from signal context.
|
||||
void sm_scanner_wake(void);
|
||||
// Run the initial full synchronization cycle.
|
||||
bool sm_scanner_run_startup_sync(void);
|
||||
// Run the steady-state scanner wait/scan loop until shutdown is requested.
|
||||
void sm_scanner_run_loop(void);
|
||||
// Release scanner service resources.
|
||||
void sm_scanner_shutdown(void);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,9 @@
|
||||
#ifndef SM_SHELLCORE_FLAGS_H
|
||||
#define SM_SHELLCORE_FLAGS_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
bool sm_shellcore_flags_start(void);
|
||||
void sm_shellcore_flags_stop(void);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,12 @@
|
||||
#ifndef SM_STABILITY_H
|
||||
#define SM_STABILITY_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
// Check whether a path is old enough since its latest mtime/ctime change.
|
||||
bool is_path_stable_now(const char *path, double *root_diff_out,
|
||||
int *stat_errno_out);
|
||||
// Wait briefly for a path to become stable before giving up.
|
||||
bool wait_for_stability_fast(const char *path, const char *name);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,9 @@
|
||||
#ifndef SM_TIME_H
|
||||
#define SM_TIME_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// Return a monotonic timestamp in microseconds, or 0 on failure.
|
||||
uint64_t monotonic_time_us(void);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,27 @@
|
||||
#ifndef SM_TITLE_STATE_H
|
||||
#define SM_TITLE_STATE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// Return whether registration was already attempted for a title.
|
||||
bool was_register_attempted(const char *title_id);
|
||||
// Return the number of registration attempts for a title.
|
||||
uint8_t get_register_attempts(const char *title_id);
|
||||
// Record a registration attempt for a title.
|
||||
void mark_register_attempted(const char *title_id);
|
||||
// Clear registration attempt state after success or confirmed user removal.
|
||||
void clear_register_attempts(const char *title_id);
|
||||
// Notify once about duplicate title IDs from different paths.
|
||||
void notify_duplicate_title_once(const char *title_id, const char *path_a,
|
||||
const char *path_b);
|
||||
// Clear the one-shot duplicate notification state for a title.
|
||||
void clear_duplicate_title_notification(const char *title_id);
|
||||
// Return the number of failed install/remount attempts for a title.
|
||||
uint8_t get_failed_mount_attempts(const char *title_id);
|
||||
// Clear failed install/remount attempts for a title.
|
||||
void clear_failed_mount_attempts(const char *title_id);
|
||||
// Increment and return failed install/remount attempts for a title.
|
||||
uint8_t bump_failed_mount_attempts(const char *title_id);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,155 @@
|
||||
#ifndef SM_TYPES_H
|
||||
#define SM_TYPES_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "sm_limits.h"
|
||||
|
||||
typedef struct {
|
||||
// +0x00: 1 -> mount read-only, 0 -> allow write.
|
||||
uint32_t ro;
|
||||
// +0x04: reserved in observed Shell/FSMP callers.
|
||||
uint32_t reserved0;
|
||||
// +0x08: logical budget/domain string, usually "game" or "system".
|
||||
const char *budget_id;
|
||||
// +0x10: reserved in observed Shell/FSMP callers.
|
||||
uint32_t reserved1;
|
||||
// +0x14: bitmask consumed by devpfs mount logic.
|
||||
uint32_t flags;
|
||||
// +0x18: optional "maxpkgszingib" value (GiB), 0 means not set.
|
||||
uint64_t max_pkg_gib;
|
||||
} devpfs_mount_opt_t;
|
||||
|
||||
typedef struct {
|
||||
// Human-readable profile id for logs.
|
||||
const char *name;
|
||||
// Raw option payload mapped to FSMP mount behavior.
|
||||
devpfs_mount_opt_t opt;
|
||||
} devpfs_mount_profile_t;
|
||||
|
||||
typedef struct {
|
||||
// Source object class (observed: 1=file, 2=device-like/special source).
|
||||
uint16_t source_type;
|
||||
// Layer descriptor flags (observed bit0 = no bitmap file path).
|
||||
uint16_t flags;
|
||||
// Must be zero.
|
||||
uint32_t reserved0;
|
||||
// Backing file or device path.
|
||||
const char *path;
|
||||
// Data start offset in backing object (bytes).
|
||||
uint64_t offset;
|
||||
// Data size exposed via this layer (bytes).
|
||||
uint64_t size;
|
||||
// Optional bitmap file path.
|
||||
const char *bitmap_path;
|
||||
// Bitmap offset in bitmap file (bytes).
|
||||
uint64_t bitmap_offset;
|
||||
// Bitmap size (bytes), 0 when bitmap is unused.
|
||||
uint64_t bitmap_size;
|
||||
} lvd_ioctl_layer_v0_t;
|
||||
|
||||
typedef struct {
|
||||
// Protocol version for /dev/lvdctl ioctl payload (0 = V0/base attach).
|
||||
uint32_t io_version;
|
||||
// Input: usually -1 for auto-assign. Output: created lvd unit id.
|
||||
int32_t device_id;
|
||||
// User-visible sector size exported by /dev/lvdN.
|
||||
uint32_t sector_size;
|
||||
// Secondary unit/granularity validated against sector_size.
|
||||
uint32_t secondary_unit;
|
||||
// Normalized attach flags produced from wrapper raw options.
|
||||
uint16_t flags;
|
||||
// LVD image type id (validator accepts 0..0xC).
|
||||
uint16_t image_type;
|
||||
// Number of valid entries in layers_ptr.
|
||||
uint32_t layer_count;
|
||||
// Total exported virtual size (bytes).
|
||||
uint64_t device_size;
|
||||
// Pointer to V0 layer array in user payload.
|
||||
lvd_ioctl_layer_v0_t *layers_ptr;
|
||||
} lvd_ioctl_attach_v0_t;
|
||||
|
||||
typedef struct {
|
||||
// Must be zero.
|
||||
uint32_t reserved0;
|
||||
// Target lvd unit id to detach.
|
||||
int32_t device_id;
|
||||
// Reserved padding required by kernel ABI.
|
||||
uint8_t reserved[0x20];
|
||||
} lvd_ioctl_detach_t;
|
||||
|
||||
struct AppDbTitleList {
|
||||
char(*ids)[MAX_TITLE_ID];
|
||||
int count;
|
||||
int capacity;
|
||||
};
|
||||
|
||||
typedef struct scan_candidate {
|
||||
char path[MAX_PATH];
|
||||
char title_id[MAX_TITLE_ID];
|
||||
char title_name[MAX_TITLE_NAME];
|
||||
char manual_source_path[MAX_PATH];
|
||||
bool installed;
|
||||
bool in_app_db;
|
||||
bool manual;
|
||||
} scan_candidate_t;
|
||||
|
||||
typedef enum {
|
||||
ATTACH_BACKEND_NONE = 0,
|
||||
// /dev/lvdctl -> /dev/lvdN
|
||||
ATTACH_BACKEND_LVD,
|
||||
// /dev/mdctl -> /dev/mdN
|
||||
ATTACH_BACKEND_MD,
|
||||
} attach_backend_t;
|
||||
|
||||
typedef struct runtime_config {
|
||||
bool debug_enabled;
|
||||
bool quiet_mode;
|
||||
bool mount_read_only;
|
||||
bool force_mount;
|
||||
bool app_install_all_enabled;
|
||||
bool app_install_all_forced;
|
||||
bool backport_fakelib_enabled;
|
||||
bool global_fakelib_enabled;
|
||||
bool global_fakelib_mount_first;
|
||||
bool kstuff_game_auto_toggle;
|
||||
bool kstuff_crash_detection_enabled;
|
||||
bool legacy_recursive_scan_forced;
|
||||
bool watch_subdirs;
|
||||
bool watch_image_files;
|
||||
char global_fakelib_path[MAX_PATH];
|
||||
uint32_t global_fakelib_exclude_title_count;
|
||||
char global_fakelib_exclude_title_ids[MAX_FAKELIB_EXCLUDE_RULES][MAX_TITLE_ID];
|
||||
uint32_t scan_depth;
|
||||
uint32_t scan_interval_us;
|
||||
uint32_t stability_wait_seconds;
|
||||
uint32_t kstuff_pause_delay_image_seconds;
|
||||
uint32_t kstuff_pause_delay_direct_seconds;
|
||||
attach_backend_t exfat_backend;
|
||||
attach_backend_t ufs_backend;
|
||||
uint32_t lvd_sector_exfat;
|
||||
uint32_t lvd_sector_ufs;
|
||||
uint32_t lvd_sector_pfs;
|
||||
uint32_t md_sector_exfat;
|
||||
uint32_t md_sector_ufs;
|
||||
} runtime_config_t;
|
||||
|
||||
typedef enum {
|
||||
IMAGE_FS_UNKNOWN = 0,
|
||||
IMAGE_FS_UFS,
|
||||
IMAGE_FS_EXFAT,
|
||||
IMAGE_FS_PFS,
|
||||
IMAGE_FS_PFSC_CONTAINER,
|
||||
} image_fs_type_t;
|
||||
|
||||
typedef struct sm_error {
|
||||
int code;
|
||||
char subsystem[16];
|
||||
char message[256];
|
||||
char path[MAX_PATH];
|
||||
bool valid;
|
||||
bool notified;
|
||||
} sm_error_t;
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,53 @@
|
||||
@echo off
|
||||
setlocal EnableExtensions
|
||||
|
||||
REM make_image.bat
|
||||
REM Usage: make_image.bat "C:\images\data.exfat" "C:\payload"
|
||||
|
||||
if "%~1"=="" goto :usage
|
||||
if "%~2"=="" goto :usage
|
||||
|
||||
set "IMAGE=%~1"
|
||||
set "SRCDIR=%~2"
|
||||
|
||||
REM Script is expected to be in the same directory as this BAT
|
||||
set "SCRIPT=%~dp0New-OsfExfatImage.ps1"
|
||||
|
||||
if not exist "%SCRIPT%" (
|
||||
echo [ERROR] PowerShell script not found: "%SCRIPT%"
|
||||
echo Put New-OsfExfatImage.ps1 next to this .bat file.
|
||||
exit /b 2
|
||||
)
|
||||
|
||||
if not exist "%SRCDIR%" (
|
||||
echo [ERROR] Source directory not found: "%SRCDIR%"
|
||||
exit /b 3
|
||||
)
|
||||
|
||||
if not exist "%SRCDIR%\eboot.bin" (
|
||||
echo [ERROR] eboot.bin not found in source directory: "%SRCDIR%"
|
||||
exit /b 4
|
||||
)
|
||||
|
||||
REM Run elevated? This BAT does not auto-elevate.
|
||||
REM Right-click -> Run as administrator, or start cmd as admin.
|
||||
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT%" -ImagePath "%IMAGE%" -SourceDir "%SRCDIR%" -ForceOverwrite
|
||||
|
||||
set "RC=%ERRORLEVEL%"
|
||||
if not "%RC%"=="0" (
|
||||
echo [ERROR] Failed with exit code %RC%.
|
||||
exit /b %RC%
|
||||
)
|
||||
|
||||
echo [OK] Done: "%IMAGE%"
|
||||
exit /b 0
|
||||
|
||||
:usage
|
||||
echo Usage:
|
||||
echo %~nx0 "C:\path\to\image.img" "C:\path\to\folder"
|
||||
echo.
|
||||
echo Notes:
|
||||
echo - Run this BAT as Administrator.
|
||||
echo - Image will be auto-sized.
|
||||
exit /b 1
|
||||
+71
-11
@@ -1,6 +1,6 @@
|
||||
#!/bin/sh
|
||||
# For Ubuntu/Debian: sudo apt-get install -y exfatprogs exfat-fuse fuse3 rsync
|
||||
# Create a exFAT image from a directory
|
||||
# For WSL2/Ubuntu/Debian: sudo apt-get install -y exfatprogs exfat-fuse fuse3 rsync
|
||||
# Create an exFAT image from a directory
|
||||
# Usage: mkexfat.sh <input_dir> [output_file]
|
||||
|
||||
set -e
|
||||
@@ -11,25 +11,85 @@ if [ -z "$1" ]; then
|
||||
fi
|
||||
|
||||
INPUT_DIR="$1"
|
||||
OUTPUT="${2:-test.ffpkg}"
|
||||
OUTPUT="${2:-test.exfat}"
|
||||
|
||||
# Calculate required size (dir size + 20% overhead + 10MB slack)
|
||||
BYTES=$(du -s -k "$INPUT_DIR" | awk '{print $1 * 1024}')
|
||||
OVERHEAD=$((BYTES / 5)) # 20%
|
||||
SLACK=$((10 * 1024 * 1024)) # 10MB
|
||||
TOTAL=$((BYTES + OVERHEAD + SLACK))
|
||||
if [ ! -d "$INPUT_DIR" ]; then
|
||||
echo "Error: input directory not found: $INPUT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$INPUT_DIR/eboot.bin" ]; then
|
||||
echo "Error: eboot.bin not found in source directory: $INPUT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# More accurate sizing for exFAT:
|
||||
# - file payload rounded to cluster size
|
||||
# - FAT + allocation bitmap estimates from cluster count
|
||||
# - directory/entry metadata estimate
|
||||
# - fixed metadata and runtime headroom
|
||||
CLUSTER_SIZE=32768
|
||||
MKFS_CLUSTER_ARG="32K"
|
||||
LARGE_FILE_THRESHOLD=$((1024 * 1024))
|
||||
META_FIXED=$((32 * 1024 * 1024)) # boot region, upcase, root and misc
|
||||
MIN_SLACK=$((64 * 1024 * 1024)) # minimum copy/runtime safety margin
|
||||
SPARE_MIN=$((64 * 1024 * 1024)) # lower bound for dynamic headroom
|
||||
SPARE_MAX=$((512 * 1024 * 1024)) # upper bound for dynamic headroom
|
||||
ENTRY_META_BYTES=256
|
||||
|
||||
FILE_COUNT=$(find "$INPUT_DIR" -type f | wc -l | tr -d ' ')
|
||||
DIR_COUNT=$(find "$INPUT_DIR" -type d | wc -l | tr -d ' ')
|
||||
RAW_FILE_BYTES=$(find "$INPUT_DIR" -type f -printf '%s\n' | \
|
||||
awk '{s += $1} END {print s + 0}')
|
||||
|
||||
AVG_FILE_BYTES=0
|
||||
if [ "$FILE_COUNT" -gt 0 ]; then
|
||||
AVG_FILE_BYTES=$((RAW_FILE_BYTES / FILE_COUNT))
|
||||
fi
|
||||
|
||||
# exFAT profile selection (same idea as UFS2 profile):
|
||||
# - large-file sets: 64K cluster
|
||||
# - small/mixed-file sets: 32K cluster
|
||||
if [ "$AVG_FILE_BYTES" -ge "$LARGE_FILE_THRESHOLD" ]; then
|
||||
CLUSTER_SIZE=65536
|
||||
MKFS_CLUSTER_ARG="64K"
|
||||
fi
|
||||
|
||||
DATA_BYTES=$(find "$INPUT_DIR" -type f -printf '%s\n' | \
|
||||
awk -v cls="$CLUSTER_SIZE" '{s += int(($1 + cls - 1) / cls) * cls} END {print s + 0}')
|
||||
DATA_CLUSTERS=$(( (DATA_BYTES + CLUSTER_SIZE - 1) / CLUSTER_SIZE ))
|
||||
FAT_BYTES=$((DATA_CLUSTERS * 4))
|
||||
BITMAP_BYTES=$(( (DATA_CLUSTERS + 7) / 8 ))
|
||||
ENTRY_BYTES=$(( (FILE_COUNT + DIR_COUNT) * ENTRY_META_BYTES ))
|
||||
|
||||
BASE_TOTAL=$((DATA_BYTES + FAT_BYTES + BITMAP_BYTES + ENTRY_BYTES + META_FIXED))
|
||||
SPARE_BYTES=$((BASE_TOTAL / 200)) # ~0.5%
|
||||
if [ "$SPARE_BYTES" -lt "$SPARE_MIN" ]; then
|
||||
SPARE_BYTES=$SPARE_MIN
|
||||
fi
|
||||
if [ "$SPARE_BYTES" -gt "$SPARE_MAX" ]; then
|
||||
SPARE_BYTES=$SPARE_MAX
|
||||
fi
|
||||
TOTAL=$((BASE_TOTAL + SPARE_BYTES))
|
||||
MIN_TOTAL=$((RAW_FILE_BYTES + MIN_SLACK))
|
||||
if [ "$TOTAL" -lt "$MIN_TOTAL" ]; then
|
||||
TOTAL=$MIN_TOTAL
|
||||
fi
|
||||
|
||||
# Round up to nearest MB
|
||||
MB=$(( (TOTAL + 1024*1024 - 1) / (1024*1024) ))
|
||||
|
||||
echo "Input size: $BYTES bytes"
|
||||
echo "Input size (raw files): $RAW_FILE_BYTES bytes"
|
||||
echo "Input size (exFAT alloc): $DATA_BYTES bytes"
|
||||
echo "Files: $FILE_COUNT, Dirs: $DIR_COUNT"
|
||||
echo "exFAT profile: -c $MKFS_CLUSTER_ARG (avg file=$AVG_FILE_BYTES bytes)"
|
||||
echo "Image size: ${MB}MB"
|
||||
|
||||
truncate -s "${MB}M" "$OUTPUT"
|
||||
mkfs.exfat -c 4K "$OUTPUT"
|
||||
mkfs.exfat -c "$MKFS_CLUSTER_ARG" "$OUTPUT"
|
||||
mkdir -p /mnt/exfat
|
||||
mount -t exfat-fuse -o loop "$OUTPUT" /mnt/exfat
|
||||
rsync -r --info=progress2 "$INPUT_DIR" /mnt/exfat/
|
||||
rsync -r --info=progress2 "$INPUT_DIR"/ /mnt/exfat/
|
||||
|
||||
umount /mnt/exfat
|
||||
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/bin/sh
|
||||
# macOS exFAT image builder — requires macOS 10.6.5+ (exFAT and rsync built-in)
|
||||
# Usage: mkexfat_macos.sh <input_dir> [output_file]
|
||||
# (c) @drakmor,@betmoar
|
||||
|
||||
set -e
|
||||
|
||||
# ── argument validation ───────────────────────────────────────────────────────
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 <input_dir> [output_file]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
INPUT_DIR="$1"
|
||||
OUTPUT="${2:-test.exfat}"
|
||||
# newfs_exfat interprets relative paths as device names under /dev/ — resolve to absolute
|
||||
OUTPUT="$(cd "$(dirname "$OUTPUT")" && pwd)/$(basename "$OUTPUT")"
|
||||
|
||||
if [ ! -d "$INPUT_DIR" ]; then
|
||||
echo "Error: input directory not found: $INPUT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$INPUT_DIR/eboot.bin" ]; then
|
||||
echo "Error: eboot.bin not found in source directory: $INPUT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── sizing constants ──────────────────────────────────────────────────────────
|
||||
|
||||
CLUSTER_SIZE=32768
|
||||
LARGE_FILE_THRESHOLD=$((1024 * 1024))
|
||||
META_FIXED=$((32 * 1024 * 1024)) # boot region, upcase, root and misc
|
||||
MIN_SLACK=$((64 * 1024 * 1024)) # minimum copy/runtime safety margin
|
||||
SPARE_MIN=$((64 * 1024 * 1024)) # lower bound for dynamic headroom
|
||||
SPARE_MAX=$((512 * 1024 * 1024)) # upper bound for dynamic headroom
|
||||
ENTRY_META_BYTES=256
|
||||
|
||||
# Single filesystem traversal: collect all file sizes into a temp file.
|
||||
# FILE_COUNT and RAW_FILE_BYTES are derived from it in one awk pass.
|
||||
# DIR_COUNT needs a separate traversal (different -type).
|
||||
SIZES_FILE=$(mktemp)
|
||||
MOUNT_POINT=""
|
||||
LOOP_DEVICE=""
|
||||
|
||||
cleanup() {
|
||||
if [ -n "$MOUNT_POINT" ] && mount | grep -q "on $MOUNT_POINT "; then
|
||||
umount "$MOUNT_POINT" 2>/dev/null || true
|
||||
fi
|
||||
if [ -n "$LOOP_DEVICE" ]; then
|
||||
hdiutil detach "$LOOP_DEVICE" 2>/dev/null || true
|
||||
fi
|
||||
if [ -n "$MOUNT_POINT" ]; then
|
||||
rmdir "$MOUNT_POINT" 2>/dev/null || true
|
||||
fi
|
||||
rm -f "$SIZES_FILE"
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
find "$INPUT_DIR" -type f -exec stat -f '%z' {} \; > "$SIZES_FILE"
|
||||
|
||||
read -r FILE_COUNT RAW_FILE_BYTES <<EOF
|
||||
$(awk '{c++; s += $1} END {print c+0, s+0}' "$SIZES_FILE")
|
||||
EOF
|
||||
|
||||
DIR_COUNT=$(find "$INPUT_DIR" -type d | wc -l | tr -d ' ')
|
||||
|
||||
AVG_FILE_BYTES=0
|
||||
if [ "$FILE_COUNT" -gt 0 ]; then
|
||||
AVG_FILE_BYTES=$((RAW_FILE_BYTES / FILE_COUNT))
|
||||
fi
|
||||
|
||||
# exFAT profile selection:
|
||||
# - large-file sets: 64K cluster
|
||||
# - small/mixed-file sets: 32K cluster
|
||||
if [ "$AVG_FILE_BYTES" -ge "$LARGE_FILE_THRESHOLD" ]; then
|
||||
CLUSTER_SIZE=65536
|
||||
fi
|
||||
|
||||
# DATA_BYTES re-reads the already-collected sizes from the temp file — no second traversal
|
||||
DATA_BYTES=$(awk -v cls="$CLUSTER_SIZE" \
|
||||
'{s += int(($1 + cls - 1) / cls) * cls} END {print s + 0}' "$SIZES_FILE")
|
||||
rm -f "$SIZES_FILE"
|
||||
|
||||
DATA_CLUSTERS=$(( (DATA_BYTES + CLUSTER_SIZE - 1) / CLUSTER_SIZE ))
|
||||
FAT_BYTES=$((DATA_CLUSTERS * 4))
|
||||
BITMAP_BYTES=$(( (DATA_CLUSTERS + 7) / 8 ))
|
||||
ENTRY_BYTES=$(( (FILE_COUNT + DIR_COUNT) * ENTRY_META_BYTES ))
|
||||
|
||||
BASE_TOTAL=$((DATA_BYTES + FAT_BYTES + BITMAP_BYTES + ENTRY_BYTES + META_FIXED))
|
||||
SPARE_BYTES=$((BASE_TOTAL / 200)) # ~0.5%
|
||||
if [ "$SPARE_BYTES" -lt "$SPARE_MIN" ]; then SPARE_BYTES=$SPARE_MIN; fi
|
||||
if [ "$SPARE_BYTES" -gt "$SPARE_MAX" ]; then SPARE_BYTES=$SPARE_MAX; fi
|
||||
TOTAL=$((BASE_TOTAL + SPARE_BYTES))
|
||||
MIN_TOTAL=$((RAW_FILE_BYTES + MIN_SLACK))
|
||||
if [ "$TOTAL" -lt "$MIN_TOTAL" ]; then TOTAL=$MIN_TOTAL; fi
|
||||
|
||||
# Round up to nearest MB
|
||||
MB=$(( (TOTAL + 1024*1024 - 1) / (1024*1024) ))
|
||||
|
||||
echo "Input size (raw files): $RAW_FILE_BYTES bytes"
|
||||
echo "Input size (exFAT alloc): $DATA_BYTES bytes"
|
||||
echo "Files: $FILE_COUNT, Dirs: $DIR_COUNT"
|
||||
echo "exFAT profile: -b ${CLUSTER_SIZE} (avg file=$AVG_FILE_BYTES bytes)"
|
||||
echo "Image size: ${MB}MB"
|
||||
|
||||
# ── create, attach, format, mount ────────────────────────────────────────────
|
||||
|
||||
MOUNT_POINT=$(mktemp -d)
|
||||
|
||||
# mkfile -n creates a sparse file without writing zeros (equivalent to Linux truncate)
|
||||
mkfile -n "${MB}m" "$OUTPUT"
|
||||
|
||||
# Attach as a block device first — newfs_exfat requires real sector geometry via ioctl
|
||||
# and cannot format a raw file directly
|
||||
ATTACH_OUTPUT=$(hdiutil attach \
|
||||
-imagekey diskimage-class=CRawDiskImage \
|
||||
-nomount "$OUTPUT")
|
||||
LOOP_DEVICE=$(printf '%s\n' "$ATTACH_OUTPUT" | awk 'NR==1 {print $1; exit}')
|
||||
if [ -z "$LOOP_DEVICE" ]; then
|
||||
echo "Error: failed to determine attached device: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# newfs_exfat -b takes bytes-per-cluster directly
|
||||
newfs_exfat -b "$CLUSTER_SIZE" "$LOOP_DEVICE"
|
||||
|
||||
mount -t exfat "$LOOP_DEVICE" "$MOUNT_POINT"
|
||||
|
||||
rsync -r --progress "$INPUT_DIR"/ "$MOUNT_POINT"/
|
||||
|
||||
umount "$MOUNT_POINT"
|
||||
hdiutil detach "$LOOP_DEVICE"
|
||||
LOOP_DEVICE="" # prevent double-detach in trap
|
||||
|
||||
echo "Created $OUTPUT"
|
||||
@@ -0,0 +1,110 @@
|
||||
#!/bin/sh
|
||||
# Create a PS4-compatible UFS2 image from a directory
|
||||
# Usage: mkufs2.sh <input_dir> [output_file]
|
||||
|
||||
set -e
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 <input_dir> [output_file]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
INPUT_DIR="$1"
|
||||
OUTPUT="${2:-download0.dat}"
|
||||
|
||||
if [ ! -d "$INPUT_DIR" ]; then
|
||||
echo "Error: input directory not found: $INPUT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$INPUT_DIR/eboot.bin" ]; then
|
||||
echo "Error: eboot.bin not found in source directory: $INPUT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# More accurate sizing for UFS2:
|
||||
# - file payload rounded to the actual allocation unit (-f)
|
||||
# - minimal per-directory allocation
|
||||
# - inode table estimate
|
||||
# - fixed filesystem metadata headroom
|
||||
|
||||
BLOCK_SIZE=65536
|
||||
FRAG_SIZE=65536
|
||||
SECTOR_SIZE=4096
|
||||
MINFREE_PERCENT=0
|
||||
DEFAULT_BYTES_PER_INODE=$((4 * FRAG_SIZE))
|
||||
MIN_BYTES_PER_INODE=$FRAG_SIZE
|
||||
INODE_SIZE=256
|
||||
INODE_SPARE=2048
|
||||
META_FIXED=$((64 * 1024 * 1024)) # superblocks, cg metadata, allocator slack
|
||||
MIN_SLACK=$((64 * 1024 * 1024)) # minimum copy/runtime safety margin
|
||||
SPARE_MIN=$((64 * 1024 * 1024)) # lower bound for dynamic headroom
|
||||
SPARE_MAX=$((512 * 1024 * 1024)) # upper bound for dynamic headroom
|
||||
|
||||
FILE_COUNT=$(find "$INPUT_DIR" -type f | wc -l | tr -d ' ')
|
||||
DIR_COUNT=$(find "$INPUT_DIR" -type d | wc -l | tr -d ' ')
|
||||
RAW_FILE_BYTES=$(find "$INPUT_DIR" -type f -exec stat -f '%z' {} + | \
|
||||
awk '{s += $1} END {print s + 0}')
|
||||
|
||||
DATA_BYTES=$(find "$INPUT_DIR" -type f -exec stat -f '%z' {} + | \
|
||||
awk -v frag="$FRAG_SIZE" '{s += int(($1 + frag - 1) / frag) * frag} END {print s + 0}')
|
||||
DIR_BYTES=$((DIR_COUNT * FRAG_SIZE))
|
||||
INODE_COUNT=$((FILE_COUNT + DIR_COUNT + INODE_SPARE))
|
||||
INODE_BYTES=$((INODE_COUNT * INODE_SIZE))
|
||||
|
||||
BASE_TOTAL=$((DATA_BYTES + DIR_BYTES + INODE_BYTES + META_FIXED))
|
||||
SPARE_BYTES=$((BASE_TOTAL / 200)) # ~0.5%
|
||||
if [ "$SPARE_BYTES" -lt "$SPARE_MIN" ]; then
|
||||
SPARE_BYTES=$SPARE_MIN
|
||||
fi
|
||||
if [ "$SPARE_BYTES" -gt "$SPARE_MAX" ]; then
|
||||
SPARE_BYTES=$SPARE_MAX
|
||||
fi
|
||||
TOTAL=$((BASE_TOTAL + SPARE_BYTES))
|
||||
MIN_TOTAL=$((RAW_FILE_BYTES + MIN_SLACK))
|
||||
if [ "$TOTAL" -lt "$MIN_TOTAL" ]; then
|
||||
TOTAL=$MIN_TOTAL
|
||||
fi
|
||||
|
||||
# Round up to nearest MB
|
||||
MB=$(( (TOTAL + 1024*1024 - 1) / (1024*1024) ))
|
||||
IMAGE_BYTES=$((MB * 1024 * 1024))
|
||||
|
||||
# Tune inode density for the actual tree size. Keep the documented default
|
||||
# unless the source contains enough files/directories to require denser inodes.
|
||||
BYTES_PER_INODE=$DEFAULT_BYTES_PER_INODE
|
||||
if [ "$INODE_COUNT" -gt 0 ]; then
|
||||
AUTO_BYTES_PER_INODE=$((IMAGE_BYTES / INODE_COUNT))
|
||||
AUTO_BYTES_PER_INODE=$(( (AUTO_BYTES_PER_INODE / SECTOR_SIZE) * SECTOR_SIZE ))
|
||||
if [ "$AUTO_BYTES_PER_INODE" -lt "$MIN_BYTES_PER_INODE" ]; then
|
||||
AUTO_BYTES_PER_INODE=$MIN_BYTES_PER_INODE
|
||||
fi
|
||||
if [ "$AUTO_BYTES_PER_INODE" -lt "$BYTES_PER_INODE" ]; then
|
||||
BYTES_PER_INODE=$AUTO_BYTES_PER_INODE
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Input size (raw files): $RAW_FILE_BYTES bytes"
|
||||
echo "Input size (UFS alloc): $DATA_BYTES bytes"
|
||||
echo "Files: $FILE_COUNT, Dirs: $DIR_COUNT"
|
||||
echo "Inode target: $INODE_COUNT"
|
||||
echo "UFS profile: -b $BLOCK_SIZE -f $FRAG_SIZE -m $MINFREE_PERCENT -S $SECTOR_SIZE -i $BYTES_PER_INODE"
|
||||
echo "Image size: ${MB}MB"
|
||||
|
||||
truncate -s "${MB}M" "$OUTPUT"
|
||||
|
||||
MD=$(mdconfig -a -t vnode -f "$(realpath "$OUTPUT")")
|
||||
newfs -O 2 -b "$BLOCK_SIZE" -f "$FRAG_SIZE" -m "$MINFREE_PERCENT" -S "$SECTOR_SIZE" -i "$BYTES_PER_INODE" /dev/${MD}
|
||||
|
||||
mkdir -p /mnt
|
||||
mount /dev/${MD} /mnt
|
||||
|
||||
# Copy contents safely (includes hidden files)
|
||||
tar -C "$INPUT_DIR" -cf - . | tar -C /mnt -xpf -
|
||||
|
||||
chmod -R 777 /mnt/*
|
||||
|
||||
umount /mnt
|
||||
mdconfig -d -u ${MD}
|
||||
|
||||
echo "Created $OUTPUT"
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,3 @@
|
||||
asm(".global \"C49jelxiaVE\"\n"
|
||||
".type \"C49jelxiaVE\" @function\n"
|
||||
"\"C49jelxiaVE\":\n");
|
||||
+423
-1637
File diff suppressed because it is too large.
Load diff
+538
@@ -0,0 +1,538 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <pthread.h>
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_types.h"
|
||||
#include "sm_appdb.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_paths.h"
|
||||
|
||||
static sqlite3 *g_app_db;
|
||||
static sqlite3_stmt *g_app_db_stmt_update_snd0;
|
||||
static sqlite3_stmt *g_app_db_stmt_normalize_snd0;
|
||||
static struct AppDbTitleList g_app_db_title_cache;
|
||||
static struct AppDbTitleList g_app_db_blocked_uninstall_ppsa_cache;
|
||||
static bool g_app_db_title_cache_ready = false;
|
||||
static bool g_app_db_blocked_uninstall_ppsa_cache_ready = false;
|
||||
static time_t g_app_db_title_cache_mtime = 0;
|
||||
static time_t g_app_db_blocked_uninstall_ppsa_cache_mtime = 0;
|
||||
static pthread_mutex_t g_app_db_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
#define APP_DB_STARTUP_MAINTENANCE_RETRIES 3
|
||||
#define APP_DB_STARTUP_MAINTENANCE_BUSY_TIMEOUT_MS 250
|
||||
|
||||
static void close_app_db(void) {
|
||||
if (g_app_db_stmt_update_snd0) {
|
||||
sqlite3_finalize(g_app_db_stmt_update_snd0);
|
||||
g_app_db_stmt_update_snd0 = NULL;
|
||||
}
|
||||
if (g_app_db_stmt_normalize_snd0) {
|
||||
sqlite3_finalize(g_app_db_stmt_normalize_snd0);
|
||||
g_app_db_stmt_normalize_snd0 = NULL;
|
||||
}
|
||||
if (g_app_db) {
|
||||
sqlite3_close(g_app_db);
|
||||
g_app_db = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void free_app_db_title_list(struct AppDbTitleList *list) {
|
||||
free(list->ids);
|
||||
list->ids = NULL;
|
||||
list->count = 0;
|
||||
list->capacity = 0;
|
||||
}
|
||||
|
||||
void shutdown_app_db(void) {
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
free_app_db_title_list(&g_app_db_title_cache);
|
||||
free_app_db_title_list(&g_app_db_blocked_uninstall_ppsa_cache);
|
||||
g_app_db_title_cache_ready = false;
|
||||
g_app_db_blocked_uninstall_ppsa_cache_ready = false;
|
||||
g_app_db_title_cache_mtime = 0;
|
||||
g_app_db_blocked_uninstall_ppsa_cache_mtime = 0;
|
||||
close_app_db();
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
}
|
||||
|
||||
static bool ensure_app_db_open(void) {
|
||||
if (!g_app_db) {
|
||||
if (sqlite3_open_v2(APP_DB_PATH, &g_app_db, SQLITE_OPEN_READWRITE, NULL) !=
|
||||
SQLITE_OK) {
|
||||
log_debug(" [DB] open failed: %s",
|
||||
(g_app_db ? sqlite3_errmsg(g_app_db) : APP_DB_PATH));
|
||||
close_app_db();
|
||||
return false;
|
||||
}
|
||||
(void)sqlite3_busy_timeout(g_app_db, APP_DB_BUSY_TIMEOUT_MS);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool app_db_wait_retry(int rc, int attempt, int max_attempts,
|
||||
bool close_before_retry) {
|
||||
if (rc != SQLITE_BUSY && rc != SQLITE_LOCKED)
|
||||
return false;
|
||||
if (attempt + 1 >= max_attempts || should_stop_requested())
|
||||
return false;
|
||||
if (close_before_retry)
|
||||
close_app_db();
|
||||
sceKernelUsleep(APP_DB_BUSY_RETRY_SLEEP_US);
|
||||
return true;
|
||||
}
|
||||
|
||||
static int app_db_prepare_with_retry(const char *sql, sqlite3_stmt **stmt_out,
|
||||
int max_attempts, const char *label) {
|
||||
for (int attempt = 0; attempt < max_attempts; attempt++) {
|
||||
if (!ensure_app_db_open())
|
||||
return -1;
|
||||
|
||||
int rc = sqlite3_prepare_v2(g_app_db, sql, -1, stmt_out, NULL);
|
||||
if (rc == SQLITE_OK)
|
||||
return SQLITE_OK;
|
||||
|
||||
if (app_db_wait_retry(rc, attempt, max_attempts, true))
|
||||
continue;
|
||||
|
||||
log_debug(" [DB] prepare failed for %s: rc=%d err=%s", label, rc,
|
||||
(g_app_db ? sqlite3_errmsg(g_app_db) : "unknown"));
|
||||
close_app_db();
|
||||
return rc;
|
||||
}
|
||||
|
||||
close_app_db();
|
||||
return -1;
|
||||
}
|
||||
|
||||
static bool app_db_exec_with_retry(const char *sql, int max_attempts,
|
||||
const char *label, int *changes_out) {
|
||||
if (changes_out)
|
||||
*changes_out = 0;
|
||||
|
||||
for (int attempt = 0; attempt < max_attempts; attempt++) {
|
||||
if (!ensure_app_db_open())
|
||||
return false;
|
||||
|
||||
char *err_msg = NULL;
|
||||
int rc = sqlite3_exec(g_app_db, sql, NULL, NULL, &err_msg);
|
||||
if (rc == SQLITE_OK) {
|
||||
if (changes_out)
|
||||
*changes_out = sqlite3_changes(g_app_db);
|
||||
sqlite3_free(err_msg);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (app_db_wait_retry(rc, attempt, max_attempts, false)) {
|
||||
sqlite3_free(err_msg);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (err_msg) {
|
||||
log_debug(" [DB] exec failed for %s: rc=%d err=%s", label, rc, err_msg);
|
||||
} else {
|
||||
log_debug(" [DB] exec failed for %s: rc=%d err=%s", label, rc,
|
||||
sqlite3_errmsg(g_app_db));
|
||||
}
|
||||
sqlite3_free(err_msg);
|
||||
|
||||
close_app_db();
|
||||
return false;
|
||||
}
|
||||
|
||||
close_app_db();
|
||||
return false;
|
||||
}
|
||||
|
||||
static int app_db_query_int(const char *sql, const char *label) {
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
int prep_rc = app_db_prepare_with_retry(sql, &stmt,
|
||||
APP_DB_STARTUP_MAINTENANCE_RETRIES,
|
||||
label);
|
||||
if (prep_rc != SQLITE_OK)
|
||||
return -1;
|
||||
|
||||
int result = -1;
|
||||
for (int attempt = 0; attempt < APP_DB_QUERY_BUSY_RETRIES; attempt++) {
|
||||
int rc = sqlite3_step(stmt);
|
||||
if (rc == SQLITE_ROW) {
|
||||
result = sqlite3_column_int(stmt, 0);
|
||||
break;
|
||||
}
|
||||
if (app_db_wait_retry(rc, attempt, APP_DB_QUERY_BUSY_RETRIES, false))
|
||||
continue;
|
||||
|
||||
log_debug(" [DB] query failed for %s: rc=%d err=%s", label, rc,
|
||||
sqlite3_errmsg(g_app_db));
|
||||
break;
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool app_db_run_startup_maintenance(void) {
|
||||
bool ok = false;
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
|
||||
if (!ensure_app_db_open())
|
||||
goto out;
|
||||
(void)sqlite3_busy_timeout(g_app_db,
|
||||
APP_DB_STARTUP_MAINTENANCE_BUSY_TIMEOUT_MS);
|
||||
|
||||
const char *backfill_count_sql =
|
||||
"SELECT COUNT(*) FROM tbl_contentinfo "
|
||||
"WHERE instr(snd0Info, '/user/app/') > 0 "
|
||||
"OR instr(snd0Info, '/sce_sys') > 0;";
|
||||
int pending_backfill =
|
||||
app_db_query_int(backfill_count_sql, "snd0info normalize backfill check");
|
||||
if (pending_backfill < 0) {
|
||||
close_app_db();
|
||||
goto out;
|
||||
}
|
||||
|
||||
int changes = 0;
|
||||
if (pending_backfill > 0) {
|
||||
const char *backfill_sql =
|
||||
"UPDATE tbl_contentinfo "
|
||||
"SET snd0Info = replace(replace(snd0Info, '/user/app/', "
|
||||
"'/user/appmeta/'), '/sce_sys', '') "
|
||||
"WHERE instr(snd0Info, '/user/app/') > 0 "
|
||||
"OR instr(snd0Info, '/sce_sys') > 0;";
|
||||
|
||||
if (!app_db_exec_with_retry(backfill_sql,
|
||||
APP_DB_STARTUP_MAINTENANCE_RETRIES,
|
||||
"snd0info startup normalize", &changes))
|
||||
goto out;
|
||||
}
|
||||
|
||||
log_debug(" [DB] snd0info startup maintenance done rows=%d pending=%d",
|
||||
changes, pending_backfill);
|
||||
|
||||
close_app_db();
|
||||
ok = true;
|
||||
|
||||
out:
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
return ok;
|
||||
}
|
||||
|
||||
int update_snd0info(const char *title_id) {
|
||||
int result = -1;
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
|
||||
if (!g_app_db_stmt_update_snd0) {
|
||||
const char *sql =
|
||||
"UPDATE tbl_contentinfo "
|
||||
"SET snd0info = '/user/appmeta/' || ?1 || '/snd0.at9' "
|
||||
"WHERE titleId = ?1;";
|
||||
int prep_rc = app_db_prepare_with_retry(sql, &g_app_db_stmt_update_snd0,
|
||||
APP_DB_PREPARE_BUSY_RETRIES,
|
||||
"snd0info update");
|
||||
if (prep_rc != SQLITE_OK)
|
||||
goto out;
|
||||
}
|
||||
|
||||
for (int attempt = 0; attempt < APP_DB_UPDATE_BUSY_RETRIES; attempt++) {
|
||||
sqlite3_reset(g_app_db_stmt_update_snd0);
|
||||
sqlite3_clear_bindings(g_app_db_stmt_update_snd0);
|
||||
if (sqlite3_bind_text(g_app_db_stmt_update_snd0, 1, title_id, -1,
|
||||
SQLITE_TRANSIENT) != SQLITE_OK) {
|
||||
log_debug(" [DB] bind failed for snd0info update: %s",
|
||||
sqlite3_errmsg(g_app_db));
|
||||
close_app_db();
|
||||
goto out;
|
||||
}
|
||||
|
||||
int rc = sqlite3_step(g_app_db_stmt_update_snd0);
|
||||
if (rc == SQLITE_DONE) {
|
||||
int changes = sqlite3_changes(g_app_db);
|
||||
close_app_db();
|
||||
result = changes;
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (app_db_wait_retry(rc, attempt, APP_DB_UPDATE_BUSY_RETRIES, false))
|
||||
continue;
|
||||
|
||||
log_debug(" [DB] step failed for snd0info update: rc=%d err=%s", rc,
|
||||
sqlite3_errmsg(g_app_db));
|
||||
close_app_db();
|
||||
goto out;
|
||||
}
|
||||
|
||||
close_app_db();
|
||||
out:
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
return result;
|
||||
}
|
||||
|
||||
int normalize_snd0info_for_title(const char *title_id) {
|
||||
int result = -1;
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
|
||||
if (!g_app_db_stmt_normalize_snd0) {
|
||||
const char *sql =
|
||||
"UPDATE tbl_contentinfo "
|
||||
"SET snd0Info = replace(replace(snd0Info, '/user/app/', "
|
||||
"'/user/appmeta/'), '/sce_sys', '') "
|
||||
"WHERE titleId = ?1 "
|
||||
"AND (instr(snd0Info, '/user/app/') > 0 "
|
||||
"OR instr(snd0Info, '/sce_sys') > 0);";
|
||||
int prep_rc = app_db_prepare_with_retry(sql, &g_app_db_stmt_normalize_snd0,
|
||||
APP_DB_PREPARE_BUSY_RETRIES,
|
||||
"snd0info normalize");
|
||||
if (prep_rc != SQLITE_OK)
|
||||
goto out;
|
||||
}
|
||||
|
||||
for (int attempt = 0; attempt < APP_DB_UPDATE_BUSY_RETRIES; attempt++) {
|
||||
sqlite3_reset(g_app_db_stmt_normalize_snd0);
|
||||
sqlite3_clear_bindings(g_app_db_stmt_normalize_snd0);
|
||||
if (sqlite3_bind_text(g_app_db_stmt_normalize_snd0, 1, title_id, -1,
|
||||
SQLITE_TRANSIENT) != SQLITE_OK) {
|
||||
log_debug(" [DB] bind failed for snd0info normalize: %s",
|
||||
sqlite3_errmsg(g_app_db));
|
||||
close_app_db();
|
||||
goto out;
|
||||
}
|
||||
|
||||
int rc = sqlite3_step(g_app_db_stmt_normalize_snd0);
|
||||
if (rc == SQLITE_DONE) {
|
||||
int changes = sqlite3_changes(g_app_db);
|
||||
close_app_db();
|
||||
result = changes;
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (app_db_wait_retry(rc, attempt, APP_DB_UPDATE_BUSY_RETRIES, false))
|
||||
continue;
|
||||
|
||||
log_debug(" [DB] step failed for snd0info normalize: rc=%d err=%s", rc,
|
||||
sqlite3_errmsg(g_app_db));
|
||||
close_app_db();
|
||||
goto out;
|
||||
}
|
||||
|
||||
close_app_db();
|
||||
out:
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool append_app_db_title(struct AppDbTitleList *list,
|
||||
const char *title_id) {
|
||||
if (!list || !title_id || title_id[0] == '\0')
|
||||
return true;
|
||||
|
||||
if (list->count >= list->capacity) {
|
||||
int new_capacity = (list->capacity > 0) ? (list->capacity * 2) : 1024;
|
||||
char(*new_ids)[MAX_TITLE_ID] =
|
||||
realloc(list->ids, (size_t)new_capacity * sizeof(*list->ids));
|
||||
if (!new_ids)
|
||||
return false;
|
||||
list->ids = new_ids;
|
||||
list->capacity = new_capacity;
|
||||
}
|
||||
|
||||
(void)strlcpy(list->ids[list->count], title_id, MAX_TITLE_ID);
|
||||
list->count++;
|
||||
return true;
|
||||
}
|
||||
|
||||
static int compare_title_id_str(const void *a, const void *b) {
|
||||
return strcmp((const char *)a, (const char *)b);
|
||||
}
|
||||
|
||||
bool app_db_title_list_contains(const struct AppDbTitleList *list,
|
||||
const char *title_id) {
|
||||
if (list->count <= 0)
|
||||
return false;
|
||||
return bsearch(title_id, list->ids, (size_t)list->count, sizeof(*list->ids),
|
||||
compare_title_id_str) != NULL;
|
||||
}
|
||||
|
||||
static bool step_app_db_title_query(struct AppDbTitleList *list,
|
||||
sqlite3_stmt *stmt,
|
||||
const char *label) {
|
||||
int busy_attempts = 0;
|
||||
while (!should_stop_requested()) {
|
||||
int rc = sqlite3_step(stmt);
|
||||
if (rc == SQLITE_ROW) {
|
||||
const char *title_id = (const char *)sqlite3_column_text(stmt, 0);
|
||||
if (!append_app_db_title(list, title_id)) {
|
||||
log_debug(" [DB] %s allocation failed", label);
|
||||
sqlite3_finalize(stmt);
|
||||
close_app_db();
|
||||
free_app_db_title_list(list);
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (rc == SQLITE_DONE) {
|
||||
sqlite3_finalize(stmt);
|
||||
close_app_db();
|
||||
return true;
|
||||
}
|
||||
if ((rc == SQLITE_BUSY || rc == SQLITE_LOCKED) &&
|
||||
busy_attempts + 1 < APP_DB_QUERY_BUSY_RETRIES) {
|
||||
busy_attempts++;
|
||||
sceKernelUsleep(APP_DB_BUSY_RETRY_SLEEP_US);
|
||||
continue;
|
||||
}
|
||||
|
||||
log_debug(" [DB] %s query failed: rc=%d err=%s", label, rc,
|
||||
sqlite3_errmsg(g_app_db));
|
||||
sqlite3_finalize(stmt);
|
||||
close_app_db();
|
||||
free_app_db_title_list(list);
|
||||
return false;
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
close_app_db();
|
||||
free_app_db_title_list(list);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool load_app_db_title_list(struct AppDbTitleList *list) {
|
||||
free_app_db_title_list(list);
|
||||
|
||||
const char *sql =
|
||||
"SELECT DISTINCT titleId "
|
||||
"FROM tbl_contentinfo "
|
||||
"WHERE titleId != '' "
|
||||
"ORDER BY titleId;";
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
int prep_rc = app_db_prepare_with_retry(sql, &stmt,
|
||||
APP_DB_PREPARE_BUSY_RETRIES,
|
||||
"title list query");
|
||||
if (prep_rc != SQLITE_OK)
|
||||
return false;
|
||||
|
||||
bool loaded = step_app_db_title_query(list, stmt, "title list");
|
||||
if (loaded)
|
||||
log_debug(" [DB] loaded app.db title list: %d entries", list->count);
|
||||
return loaded;
|
||||
}
|
||||
|
||||
static bool load_app_db_blocked_uninstall_ppsa_list(struct AppDbTitleList *list) {
|
||||
free_app_db_title_list(list);
|
||||
|
||||
const char *sql =
|
||||
"SELECT DISTINCT titleId "
|
||||
"FROM tbl_contentinfo "
|
||||
"WHERE titleId LIKE 'PPSA%' "
|
||||
"AND uninstallable = 0 "
|
||||
"ORDER BY titleId;";
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
int prep_rc =
|
||||
app_db_prepare_with_retry(sql, &stmt, APP_DB_PREPARE_BUSY_RETRIES,
|
||||
"blocked PPSA uninstallable query");
|
||||
if (prep_rc != SQLITE_OK)
|
||||
return false;
|
||||
|
||||
bool loaded = step_app_db_title_query(list, stmt, "blocked PPSA uninstall");
|
||||
if (loaded)
|
||||
log_debug(" [DB] loaded blocked PPSA uninstall list: %d entries",
|
||||
list->count);
|
||||
return loaded;
|
||||
}
|
||||
|
||||
void invalidate_app_db_title_cache(void) {
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
g_app_db_title_cache_ready = false;
|
||||
g_app_db_blocked_uninstall_ppsa_cache_ready = false;
|
||||
g_app_db_title_cache_mtime = 0;
|
||||
g_app_db_blocked_uninstall_ppsa_cache_mtime = 0;
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
}
|
||||
|
||||
static bool copy_app_db_title_list(struct AppDbTitleList *dst,
|
||||
const struct AppDbTitleList *src) {
|
||||
if (!dst || !src)
|
||||
return false;
|
||||
|
||||
free_app_db_title_list(dst);
|
||||
if (src->count <= 0)
|
||||
return true;
|
||||
|
||||
char(*ids)[MAX_TITLE_ID] = malloc((size_t)src->count * sizeof(*ids));
|
||||
if (!ids)
|
||||
return false;
|
||||
|
||||
memcpy(ids, src->ids, (size_t)src->count * sizeof(*ids));
|
||||
dst->ids = ids;
|
||||
dst->count = src->count;
|
||||
dst->capacity = src->count;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool get_app_db_title_list_cached(struct AppDbTitleList *list_out) {
|
||||
if (!list_out)
|
||||
return false;
|
||||
|
||||
free_app_db_title_list(list_out);
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
|
||||
struct stat st;
|
||||
int app_db_stat_rc = stat(APP_DB_PATH, &st);
|
||||
|
||||
if (!g_app_db_title_cache_ready ||
|
||||
(app_db_stat_rc == 0 && g_app_db_title_cache_mtime != st.st_mtime)) {
|
||||
struct AppDbTitleList fresh = {0};
|
||||
if (app_db_stat_rc == 0 && load_app_db_title_list(&fresh)) {
|
||||
free_app_db_title_list(&g_app_db_title_cache);
|
||||
g_app_db_title_cache = fresh;
|
||||
g_app_db_title_cache_mtime = st.st_mtime;
|
||||
g_app_db_title_cache_ready = true;
|
||||
} else {
|
||||
free_app_db_title_list(&fresh);
|
||||
if (!g_app_db_title_cache_ready) {
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool copied = copy_app_db_title_list(list_out, &g_app_db_title_cache);
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
return copied;
|
||||
}
|
||||
|
||||
bool get_app_db_blocked_uninstall_ppsa_list(struct AppDbTitleList *list_out) {
|
||||
if (!list_out)
|
||||
return false;
|
||||
|
||||
free_app_db_title_list(list_out);
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
|
||||
struct stat st;
|
||||
int app_db_stat_rc = stat(APP_DB_PATH, &st);
|
||||
|
||||
if (!g_app_db_blocked_uninstall_ppsa_cache_ready ||
|
||||
(app_db_stat_rc == 0 &&
|
||||
g_app_db_blocked_uninstall_ppsa_cache_mtime != st.st_mtime)) {
|
||||
struct AppDbTitleList fresh = {0};
|
||||
if (app_db_stat_rc == 0 &&
|
||||
load_app_db_blocked_uninstall_ppsa_list(&fresh)) {
|
||||
free_app_db_title_list(&g_app_db_blocked_uninstall_ppsa_cache);
|
||||
g_app_db_blocked_uninstall_ppsa_cache = fresh;
|
||||
g_app_db_blocked_uninstall_ppsa_cache_mtime = st.st_mtime;
|
||||
g_app_db_blocked_uninstall_ppsa_cache_ready = true;
|
||||
} else {
|
||||
free_app_db_title_list(&fresh);
|
||||
if (!g_app_db_blocked_uninstall_ppsa_cache_ready) {
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool loaded =
|
||||
copy_app_db_title_list(list_out, &g_app_db_blocked_uninstall_ppsa_cache);
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
return loaded;
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,305 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include "sm_fakelib.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_types.h"
|
||||
|
||||
#include <pthread.h>
|
||||
|
||||
typedef struct {
|
||||
char source_path[MAX_PATH];
|
||||
char mount_path[MAX_PATH];
|
||||
const char *label;
|
||||
} fakelib_layer_t;
|
||||
|
||||
typedef struct {
|
||||
pid_t pid;
|
||||
char mount_path[MAX_PATH];
|
||||
fakelib_layer_t layers[2];
|
||||
size_t layer_count;
|
||||
} fakelib_session_t;
|
||||
|
||||
static fakelib_session_t g_fakelib_mount;
|
||||
static pthread_mutex_t g_fakelib_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
bool sm_fakelib_game_feature_enabled(void) {
|
||||
return runtime_config()->backport_fakelib_enabled;
|
||||
}
|
||||
|
||||
static bool mount_fakelib_overlay(const char *title_id,
|
||||
const char *source_path,
|
||||
const char *mount_path,
|
||||
const char *label) {
|
||||
struct iovec overlay_iov[] = {
|
||||
IOVEC_ENTRY("fstype"), IOVEC_ENTRY("unionfs"),
|
||||
IOVEC_ENTRY("from"), IOVEC_ENTRY(source_path),
|
||||
IOVEC_ENTRY("fspath"), IOVEC_ENTRY(mount_path),
|
||||
IOVEC_ENTRY("copymode"), IOVEC_ENTRY("transparent"),
|
||||
IOVEC_ENTRY("notime"), IOVEC_ENTRY(NULL),
|
||||
IOVEC_ENTRY("fnodup"), IOVEC_ENTRY(NULL)};
|
||||
|
||||
if (nmount(overlay_iov, IOVEC_SIZE(overlay_iov), 0) == 0) {
|
||||
log_debug(" [FAKELIB] %s libraries mounted for %s: %s -> %s", label,
|
||||
title_id, source_path, mount_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
log_debug(" [FAKELIB] %s mount failed for %s (%s -> %s): %s", label,
|
||||
title_id, source_path, mount_path, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool unmount_fakelib_overlay(const fakelib_layer_t *layer) {
|
||||
const char *mount_path = layer->mount_path;
|
||||
if (unmount(mount_path, MNT_FORCE) == 0 || errno == ENOENT ||
|
||||
errno == EINVAL) {
|
||||
log_debug(" [FAKELIB] %s libraries unmounted: %s -> %s", layer->label,
|
||||
layer->source_path, mount_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
log_debug(" [FAKELIB] %s unmount failed for %s: %s", layer->label,
|
||||
mount_path, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool track_fakelib_overlay(const char *title_id,
|
||||
const char *source_path,
|
||||
const char *mount_path,
|
||||
const char *label) {
|
||||
if (!mount_fakelib_overlay(title_id, source_path, mount_path, label))
|
||||
return false;
|
||||
|
||||
fakelib_layer_t *layer = &g_fakelib_mount.layers[g_fakelib_mount.layer_count++];
|
||||
layer->label = label;
|
||||
(void)strlcpy(layer->source_path, source_path, sizeof(layer->source_path));
|
||||
(void)strlcpy(layer->mount_path, mount_path, sizeof(layer->mount_path));
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool resolve_sandbox_context(const char *title_id,
|
||||
char game_source_path[MAX_PATH],
|
||||
char mount_path[MAX_PATH]) {
|
||||
game_source_path[0] = '\0';
|
||||
mount_path[0] = '\0';
|
||||
|
||||
char sandbox_id[MAX_TITLE_ID];
|
||||
DIR *d = opendir("/mnt/sandbox");
|
||||
if (!d)
|
||||
return false;
|
||||
|
||||
size_t title_len = strlen(title_id);
|
||||
int best_index = -1;
|
||||
bool found = false;
|
||||
struct dirent *entry;
|
||||
while ((entry = readdir(d)) != NULL) {
|
||||
if (entry->d_name[0] == '.')
|
||||
continue;
|
||||
if (strncmp(entry->d_name, title_id, title_len) != 0)
|
||||
continue;
|
||||
if (entry->d_name[title_len] != '_')
|
||||
continue;
|
||||
|
||||
const char *suffix = entry->d_name + title_len + 1u;
|
||||
if (!isdigit((unsigned char)suffix[0]))
|
||||
continue;
|
||||
if (strlen(entry->d_name) >= sizeof(sandbox_id))
|
||||
continue;
|
||||
|
||||
errno = 0;
|
||||
long idx_long = strtol(suffix, NULL, 10);
|
||||
if (errno == ERANGE || idx_long > INT_MAX)
|
||||
continue;
|
||||
int idx = (int)idx_long;
|
||||
if (idx < best_index)
|
||||
continue;
|
||||
|
||||
best_index = idx;
|
||||
(void)strlcpy(sandbox_id, entry->d_name, sizeof(sandbox_id));
|
||||
found = true;
|
||||
}
|
||||
|
||||
closedir(d);
|
||||
if (!found)
|
||||
return false;
|
||||
|
||||
char source_path[MAX_PATH];
|
||||
snprintf(source_path, sizeof(source_path), "/mnt/sandbox/%s/app0/fakelib",
|
||||
sandbox_id);
|
||||
struct stat st;
|
||||
if (stat(source_path, &st) == 0 && S_ISDIR(st.st_mode))
|
||||
(void)strlcpy(game_source_path, source_path, MAX_PATH);
|
||||
|
||||
char sandbox_root[MAX_PATH];
|
||||
snprintf(sandbox_root, sizeof(sandbox_root), "/mnt/sandbox/%s", sandbox_id);
|
||||
d = opendir(sandbox_root);
|
||||
if (!d)
|
||||
return false;
|
||||
|
||||
found = false;
|
||||
while ((entry = readdir(d)) != NULL) {
|
||||
if (entry->d_name[0] == '.')
|
||||
continue;
|
||||
if (strcmp(entry->d_name, "app0") == 0)
|
||||
continue;
|
||||
|
||||
snprintf(mount_path, MAX_PATH, "%s/%s/common/lib", sandbox_root,
|
||||
entry->d_name);
|
||||
if (stat(mount_path, &st) != 0 || !S_ISDIR(st.st_mode))
|
||||
continue;
|
||||
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
closedir(d);
|
||||
return found;
|
||||
}
|
||||
|
||||
static bool resolve_global_fakelib_source(const char *title_id,
|
||||
char source_path[MAX_PATH]) {
|
||||
source_path[0] = '\0';
|
||||
|
||||
const runtime_config_t *cfg = runtime_config();
|
||||
if (!cfg->global_fakelib_enabled)
|
||||
return false;
|
||||
if (is_global_fakelib_excluded_for_title(title_id))
|
||||
return false;
|
||||
|
||||
struct stat st;
|
||||
if (stat(cfg->global_fakelib_path, &st) != 0) {
|
||||
if (errno != ENOENT)
|
||||
log_debug(" [FAKELIB] global path unavailable for %s: %s (%s)",
|
||||
title_id, cfg->global_fakelib_path, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (!S_ISDIR(st.st_mode)) {
|
||||
log_debug(" [FAKELIB] global path is not a directory for %s: %s",
|
||||
title_id, cfg->global_fakelib_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
(void)strlcpy(source_path, cfg->global_fakelib_path, MAX_PATH);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool cleanup_fakelib_mount(void) {
|
||||
if (g_fakelib_mount.layer_count == 0) {
|
||||
memset(&g_fakelib_mount, 0, sizeof(g_fakelib_mount));
|
||||
return true;
|
||||
}
|
||||
|
||||
while (g_fakelib_mount.layer_count > 0) {
|
||||
fakelib_layer_t *layer =
|
||||
&g_fakelib_mount.layers[g_fakelib_mount.layer_count - 1];
|
||||
if (!unmount_fakelib_overlay(layer))
|
||||
return false;
|
||||
memset(layer, 0, sizeof(*layer));
|
||||
g_fakelib_mount.layer_count--;
|
||||
}
|
||||
|
||||
memset(&g_fakelib_mount, 0, sizeof(g_fakelib_mount));
|
||||
return true;
|
||||
}
|
||||
|
||||
void sm_fakelib_game_on_exec(pid_t pid, const char *title_id) {
|
||||
pthread_mutex_lock(&g_fakelib_mutex);
|
||||
if (!sm_fakelib_game_feature_enabled()) {
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_fakelib_mount.layer_count > 0 && g_fakelib_mount.pid == pid) {
|
||||
log_debug(" [FAKELIB] already tracking pid=%ld for %s", (long)pid,
|
||||
title_id);
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_fakelib_mount.layer_count > 0 && g_fakelib_mount.pid != pid) {
|
||||
log_debug(" [FAKELIB] handoff active mount pid=%ld -> pid=%ld (%s)",
|
||||
(long)g_fakelib_mount.pid, (long)pid, title_id);
|
||||
if (!cleanup_fakelib_mount()) {
|
||||
log_debug(" [FAKELIB] handoff cleanup failed for pid=%ld, skipping %s",
|
||||
(long)g_fakelib_mount.pid, title_id);
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
char game_source_path[MAX_PATH];
|
||||
char global_source_path[MAX_PATH];
|
||||
char mount_path[MAX_PATH];
|
||||
if (!resolve_sandbox_context(title_id, game_source_path, mount_path)) {
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
bool has_global =
|
||||
resolve_global_fakelib_source(title_id, global_source_path);
|
||||
bool has_game = (game_source_path[0] != '\0');
|
||||
if (!has_global && !has_game) {
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
memset(&g_fakelib_mount, 0, sizeof(g_fakelib_mount));
|
||||
g_fakelib_mount.pid = pid;
|
||||
(void)strlcpy(g_fakelib_mount.mount_path, mount_path,
|
||||
sizeof(g_fakelib_mount.mount_path));
|
||||
|
||||
bool global_first = runtime_config()->global_fakelib_mount_first;
|
||||
bool same_source =
|
||||
has_global && has_game &&
|
||||
strcmp(global_source_path, game_source_path) == 0;
|
||||
if (global_first && has_global && !same_source) {
|
||||
if (!track_fakelib_overlay(title_id, global_source_path, mount_path,
|
||||
"global")) {
|
||||
(void)cleanup_fakelib_mount();
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (has_game) {
|
||||
if (!track_fakelib_overlay(title_id, game_source_path, mount_path,
|
||||
"game")) {
|
||||
(void)cleanup_fakelib_mount();
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!global_first && has_global && !same_source) {
|
||||
if (!track_fakelib_overlay(title_id, global_source_path, mount_path,
|
||||
"global")) {
|
||||
(void)cleanup_fakelib_mount();
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (has_game)
|
||||
notify_system_info("Game backported: %s", title_id);
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
}
|
||||
|
||||
void sm_fakelib_game_on_exit(pid_t pid) {
|
||||
pthread_mutex_lock(&g_fakelib_mutex);
|
||||
if (g_fakelib_mount.layer_count == 0 || g_fakelib_mount.pid != pid) {
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
log_debug(" [FAKELIB] game stopped: pid=%ld mount=%s", (long)pid,
|
||||
g_fakelib_mount.mount_path);
|
||||
cleanup_fakelib_mount();
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
}
|
||||
|
||||
void sm_fakelib_game_shutdown(void) {
|
||||
pthread_mutex_lock(&g_fakelib_mutex);
|
||||
(void)cleanup_fakelib_mount();
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
}
|
||||
+1163
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,198 @@
|
||||
#include "sm_platform.h"
|
||||
#include "sm_game_cache.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_filesystem.h"
|
||||
#include "sm_image_cache.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_title_state.h"
|
||||
|
||||
struct GameCache {
|
||||
char path[MAX_PATH];
|
||||
char title_id[MAX_TITLE_ID];
|
||||
char title_name[MAX_TITLE_NAME];
|
||||
char owning_scan_root[MAX_PATH];
|
||||
bool valid;
|
||||
};
|
||||
|
||||
static struct GameCache g_game_cache[MAX_PENDING];
|
||||
|
||||
static bool resolve_game_cache_owning_scan_root(const char *path,
|
||||
char owning_scan_root[MAX_PATH]) {
|
||||
char resolved_source_path[MAX_PATH];
|
||||
const char *match_path = path;
|
||||
owning_scan_root[0] = '\0';
|
||||
|
||||
if (is_under_image_mount_base(path) &&
|
||||
resolve_image_source_from_mount_cache(path, resolved_source_path,
|
||||
sizeof(resolved_source_path))) {
|
||||
match_path = resolved_source_path;
|
||||
}
|
||||
|
||||
size_t best_match_len = 0;
|
||||
for (int i = 0; i < get_scan_path_count(); i++) {
|
||||
const char *scan_path = get_scan_path(i);
|
||||
if (!path_matches_root_or_child(match_path, scan_path))
|
||||
continue;
|
||||
|
||||
size_t scan_path_len = strlen(scan_path);
|
||||
if (scan_path_len <= best_match_len)
|
||||
continue;
|
||||
|
||||
(void)strlcpy(owning_scan_root, scan_path, MAX_PATH);
|
||||
best_match_len = scan_path_len;
|
||||
}
|
||||
|
||||
return owning_scan_root[0] != '\0';
|
||||
}
|
||||
|
||||
static void write_game_cache_slot(struct GameCache *entry, const char *path,
|
||||
const char *title_id,
|
||||
const char *title_name,
|
||||
const char *owning_scan_root) {
|
||||
(void)strlcpy(entry->path, path, sizeof(entry->path));
|
||||
(void)strlcpy(entry->title_id, title_id, sizeof(entry->title_id));
|
||||
(void)strlcpy(entry->title_name, title_name, sizeof(entry->title_name));
|
||||
(void)strlcpy(entry->owning_scan_root, owning_scan_root,
|
||||
sizeof(entry->owning_scan_root));
|
||||
entry->valid = true;
|
||||
}
|
||||
|
||||
static bool ensure_game_cache_owning_scan_root(struct GameCache *entry) {
|
||||
if (entry->owning_scan_root[0] != '\0')
|
||||
return true;
|
||||
|
||||
return resolve_game_cache_owning_scan_root(entry->path, entry->owning_scan_root);
|
||||
}
|
||||
|
||||
static void clear_game_cache_slot(int index, const char *reason) {
|
||||
if (index < 0 || index >= MAX_PENDING || !g_game_cache[index].valid)
|
||||
return;
|
||||
|
||||
if (reason && reason[0] != '\0') {
|
||||
if (g_game_cache[index].title_id[0] != '\0')
|
||||
log_debug(" [CACHE] %s: %s (%s)", reason, g_game_cache[index].title_id,
|
||||
g_game_cache[index].path);
|
||||
else
|
||||
log_debug(" [CACHE] %s: %s", reason, g_game_cache[index].path);
|
||||
}
|
||||
|
||||
if (g_game_cache[index].title_id[0] != '\0')
|
||||
clear_duplicate_title_notification(g_game_cache[index].title_id);
|
||||
|
||||
memset(&g_game_cache[index], 0, sizeof(g_game_cache[index]));
|
||||
}
|
||||
|
||||
void cache_game_entry(const char *path, const char *title_id,
|
||||
const char *title_name) {
|
||||
char owning_scan_root[MAX_PATH];
|
||||
(void)resolve_game_cache_owning_scan_root(path, owning_scan_root);
|
||||
|
||||
for (int k = 0; k < MAX_PENDING; k++) {
|
||||
if (!g_game_cache[k].valid)
|
||||
continue;
|
||||
if (strcmp(g_game_cache[k].path, path) != 0 &&
|
||||
strcmp(g_game_cache[k].title_id, title_id) != 0) {
|
||||
continue;
|
||||
}
|
||||
write_game_cache_slot(&g_game_cache[k], path, title_id, title_name,
|
||||
owning_scan_root);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int k = 0; k < MAX_PENDING; k++) {
|
||||
if (!g_game_cache[k].valid) {
|
||||
write_game_cache_slot(&g_game_cache[k], path, title_id, title_name,
|
||||
owning_scan_root);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void prune_game_cache(void) {
|
||||
for (int k = 0; k < MAX_PENDING; k++) {
|
||||
if (!g_game_cache[k].valid)
|
||||
continue;
|
||||
if (path_exists(g_game_cache[k].path))
|
||||
continue;
|
||||
clear_game_cache_slot(k, "source removed");
|
||||
}
|
||||
}
|
||||
|
||||
void prune_game_cache_for_root(const char *root) {
|
||||
if (!root || root[0] == '\0') {
|
||||
prune_game_cache();
|
||||
return;
|
||||
}
|
||||
|
||||
for (int k = 0; k < MAX_PENDING; k++) {
|
||||
if (!g_game_cache[k].valid)
|
||||
continue;
|
||||
const char *entry_root =
|
||||
ensure_game_cache_owning_scan_root(&g_game_cache[k])
|
||||
? g_game_cache[k].owning_scan_root
|
||||
: g_game_cache[k].path;
|
||||
if (!path_matches_root_or_child(entry_root, root))
|
||||
continue;
|
||||
if (path_exists(g_game_cache[k].path))
|
||||
continue;
|
||||
clear_game_cache_slot(k, "source removed");
|
||||
}
|
||||
}
|
||||
|
||||
void for_each_cached_game_entry(const char *root, game_cache_iter_fn fn,
|
||||
void *ctx) {
|
||||
if (!fn)
|
||||
return;
|
||||
|
||||
for (int k = 0; k < MAX_PENDING; k++) {
|
||||
if (!g_game_cache[k].valid)
|
||||
continue;
|
||||
bool has_owning_scan_root = ensure_game_cache_owning_scan_root(&g_game_cache[k]);
|
||||
const char *entry_root =
|
||||
has_owning_scan_root ? g_game_cache[k].owning_scan_root : g_game_cache[k].path;
|
||||
if (root && root[0] != '\0' && strcmp(entry_root, root) != 0) {
|
||||
continue;
|
||||
}
|
||||
if (!fn(g_game_cache[k].path, g_game_cache[k].title_id,
|
||||
g_game_cache[k].title_name,
|
||||
has_owning_scan_root ? g_game_cache[k].owning_scan_root : NULL, ctx)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool find_cached_game(const char *path, const char *title_id,
|
||||
const char **existing_path_out) {
|
||||
if (existing_path_out)
|
||||
*existing_path_out = NULL;
|
||||
|
||||
for (int k = 0; k < MAX_PENDING; k++) {
|
||||
if (!g_game_cache[k].valid)
|
||||
continue;
|
||||
if (path && strcmp(g_game_cache[k].path, path) == 0) {
|
||||
if (existing_path_out)
|
||||
*existing_path_out = g_game_cache[k].path;
|
||||
return true;
|
||||
}
|
||||
if (title_id && title_id[0] != '\0' &&
|
||||
strcmp(g_game_cache[k].title_id, title_id) == 0) {
|
||||
if (existing_path_out)
|
||||
*existing_path_out = g_game_cache[k].path;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void clear_cached_game(const char *path) {
|
||||
for (int k = 0; k < MAX_PENDING; k++) {
|
||||
if (!g_game_cache[k].valid)
|
||||
continue;
|
||||
if (strcmp(g_game_cache[k].path, path) != 0)
|
||||
continue;
|
||||
clear_game_cache_slot(k, "removed from duplicate tracking");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,655 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <stdatomic.h>
|
||||
#include <sys/event.h>
|
||||
#include <sys/select.h>
|
||||
|
||||
#include "sm_appdb.h"
|
||||
#include "sm_fakelib.h"
|
||||
#include "sm_game_lifecycle.h"
|
||||
#include "sm_kstuff.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_mdbg.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_scanner.h"
|
||||
#include "sm_time.h"
|
||||
|
||||
#define MAX_PENDING_GAME_EXEC_CANDIDATES 32
|
||||
|
||||
typedef struct {
|
||||
bool active;
|
||||
pid_t pid;
|
||||
uint64_t exec_time_us;
|
||||
uint64_t deadline_us;
|
||||
} pending_game_launch_t;
|
||||
|
||||
// SysCore can emit multiple child exec events before the actual game title_id
|
||||
// becomes visible, so keep a small raw exec candidate buffer here.
|
||||
static pending_game_launch_t
|
||||
g_pending_game_launches[MAX_PENDING_GAME_EXEC_CANDIDATES];
|
||||
static pthread_t g_game_lifecycle_thread;
|
||||
static bool g_game_lifecycle_thread_started = false;
|
||||
static volatile sig_atomic_t g_game_lifecycle_stop_requested = 0;
|
||||
static int g_game_lifecycle_wake_pipe[2] = {-1, -1};
|
||||
static pthread_mutex_t g_game_lifecycle_start_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_cond_t g_game_lifecycle_start_cond = PTHREAD_COND_INITIALIZER;
|
||||
static bool g_game_lifecycle_start_ready = false;
|
||||
static bool g_game_lifecycle_start_success = false;
|
||||
static _Atomic pid_t g_active_game_pid = 0;
|
||||
static pthread_mutex_t g_active_game_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static char g_active_game_title_id[MAX_TITLE_ID];
|
||||
|
||||
static bool register_game_exit_watch(int kq, pid_t pid);
|
||||
|
||||
static void publish_active_game_pid(pid_t pid) {
|
||||
pid_t previous_pid = atomic_exchange(&g_active_game_pid, pid);
|
||||
if (previous_pid == pid)
|
||||
return;
|
||||
sm_scanner_wake();
|
||||
}
|
||||
|
||||
static void publish_active_game(pid_t pid, const char *title_id) {
|
||||
pthread_mutex_lock(&g_active_game_mutex);
|
||||
if (pid > 0 && title_id && title_id[0] != '\0')
|
||||
(void)strlcpy(g_active_game_title_id, title_id,
|
||||
sizeof(g_active_game_title_id));
|
||||
else
|
||||
g_active_game_title_id[0] = '\0';
|
||||
pthread_mutex_unlock(&g_active_game_mutex);
|
||||
publish_active_game_pid(pid);
|
||||
}
|
||||
|
||||
static bool consume_active_game_title(pid_t pid,
|
||||
char title_id_out[MAX_TITLE_ID]) {
|
||||
bool matched = false;
|
||||
pthread_mutex_lock(&g_active_game_mutex);
|
||||
if (pid > 0 && atomic_load(&g_active_game_pid) == pid &&
|
||||
g_active_game_title_id[0] != '\0') {
|
||||
(void)strlcpy(title_id_out, g_active_game_title_id, MAX_TITLE_ID);
|
||||
matched = true;
|
||||
}
|
||||
if (matched)
|
||||
g_active_game_title_id[0] = '\0';
|
||||
pthread_mutex_unlock(&g_active_game_mutex);
|
||||
return matched;
|
||||
}
|
||||
|
||||
static bool is_supported_title_id(const char *title_id) {
|
||||
return strncmp(title_id, "PPSA", 4) == 0 || strncmp(title_id, "CUSA", 4) == 0;
|
||||
}
|
||||
|
||||
static uint64_t min_nonzero_u64(uint64_t a, uint64_t b) {
|
||||
if (a == 0)
|
||||
return b;
|
||||
if (b == 0)
|
||||
return a;
|
||||
return a < b ? a : b;
|
||||
}
|
||||
|
||||
/*
|
||||
static void bytes_to_hex(const void *src, size_t src_len, char *dst,
|
||||
size_t dst_size) {
|
||||
static const char hex_chars[] = "0123456789abcdef";
|
||||
const uint8_t *bytes = (const uint8_t *)src;
|
||||
size_t out_pos = 0;
|
||||
|
||||
if (!dst || dst_size == 0)
|
||||
return;
|
||||
|
||||
for (size_t i = 0; i < src_len && out_pos + 2 < dst_size; ++i) {
|
||||
dst[out_pos++] = hex_chars[(bytes[i] >> 4) & 0x0f];
|
||||
dst[out_pos++] = hex_chars[bytes[i] & 0x0f];
|
||||
}
|
||||
|
||||
dst[out_pos] = '\0';
|
||||
}
|
||||
|
||||
static void log_app_info_response(pid_t pid, int rc, const app_info_t *appinfo) {
|
||||
if (rc != 0) {
|
||||
log_debug(" [GAME] sceKernelGetAppInfo pid=%ld failed: rc=0x%08X",
|
||||
(long)pid, (unsigned)rc);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!appinfo)
|
||||
return;
|
||||
|
||||
char title_id[MAX_TITLE_ID];
|
||||
size_t title_len = strnlen(appinfo->title_id, sizeof(appinfo->title_id));
|
||||
if (title_len >= sizeof(title_id))
|
||||
title_len = sizeof(title_id) - 1u;
|
||||
memcpy(title_id, appinfo->title_id, title_len);
|
||||
title_id[title_len] = '\0';
|
||||
|
||||
char unknown2_hex[sizeof(appinfo->unknown2) * 2u + 1u];
|
||||
bytes_to_hex(appinfo->unknown2, sizeof(appinfo->unknown2), unknown2_hex,
|
||||
sizeof(unknown2_hex));
|
||||
|
||||
log_debug(" [GAME] sceKernelGetAppInfo pid=%ld app_id=0x%08X "
|
||||
"unknown1=0x%016llX title_id=\"%s\"",
|
||||
(long)pid, appinfo->app_id,
|
||||
(unsigned long long)appinfo->unknown1, title_id);
|
||||
log_debug(" [GAME] sceKernelGetAppInfo pid=%ld unknown2=%s", (long)pid,
|
||||
unknown2_hex);
|
||||
}
|
||||
*/
|
||||
|
||||
static bool resolve_game_title_id(pid_t pid, char title_id[MAX_TITLE_ID],
|
||||
uint32_t *app_id_out) {
|
||||
app_info_t appinfo;
|
||||
memset(&appinfo, 0, sizeof(appinfo));
|
||||
int rc = sceKernelGetAppInfo(pid, &appinfo);
|
||||
/* log_app_info_response(pid, rc, &appinfo); */
|
||||
if (rc != 0)
|
||||
return false;
|
||||
|
||||
size_t title_len = strnlen(appinfo.title_id, sizeof(appinfo.title_id));
|
||||
if (title_len == 0 || title_len >= MAX_TITLE_ID)
|
||||
return false;
|
||||
|
||||
memcpy(title_id, appinfo.title_id, title_len);
|
||||
title_id[title_len] = '\0';
|
||||
if (app_id_out)
|
||||
*app_id_out = appinfo.app_id;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool is_process_alive(pid_t pid) {
|
||||
if (kill(pid, 0) == 0)
|
||||
return true;
|
||||
|
||||
return errno != ESRCH;
|
||||
}
|
||||
|
||||
static bool dispatch_game_launch(int kq, pid_t pid, uint64_t exec_time_us,
|
||||
const char *title_id, uint32_t app_id) {
|
||||
if (!register_game_exit_watch(kq, pid)) {
|
||||
log_debug(" [GAME] skipping launch tracking for %s pid=%ld without exit watch",
|
||||
title_id, (long)pid);
|
||||
return false;
|
||||
}
|
||||
|
||||
log_debug(" [GAME] started: %s pid=%ld app_id=0x%08X", title_id,
|
||||
(long)pid, app_id);
|
||||
publish_active_game(pid, title_id);
|
||||
sm_kstuff_game_on_exec(pid, title_id, app_id, exec_time_us);
|
||||
sm_fakelib_game_on_exec(pid, title_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void clear_pending_game_launch(pending_game_launch_t *entry) {
|
||||
memset(entry, 0, sizeof(*entry));
|
||||
}
|
||||
|
||||
static void clear_all_pending_game_launches(void) {
|
||||
memset(g_pending_game_launches, 0, sizeof(g_pending_game_launches));
|
||||
}
|
||||
|
||||
static void close_game_lifecycle_wake_pipe(void) {
|
||||
if (g_game_lifecycle_wake_pipe[0] >= 0) {
|
||||
close(g_game_lifecycle_wake_pipe[0]);
|
||||
g_game_lifecycle_wake_pipe[0] = -1;
|
||||
}
|
||||
if (g_game_lifecycle_wake_pipe[1] >= 0) {
|
||||
close(g_game_lifecycle_wake_pipe[1]);
|
||||
g_game_lifecycle_wake_pipe[1] = -1;
|
||||
}
|
||||
}
|
||||
|
||||
static void drain_game_lifecycle_wake_pipe(void) {
|
||||
char drain_buf[32];
|
||||
while (read(g_game_lifecycle_wake_pipe[0], drain_buf, sizeof(drain_buf)) > 0) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool discard_game_lifecycle_events_nowait(int kq) {
|
||||
struct kevent events[16];
|
||||
struct timespec timeout;
|
||||
memset(&timeout, 0, sizeof(timeout));
|
||||
|
||||
while (true) {
|
||||
int nev = kevent(kq, NULL, 0, events, sizeof(events) / sizeof(events[0]),
|
||||
&timeout);
|
||||
if (nev < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
log_debug(" [GAME] stale event drain failed: %s", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (nev == 0)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static bool set_fd_nonblocking(int fd) {
|
||||
int flags = fcntl(fd, F_GETFL, 0);
|
||||
if (flags < 0)
|
||||
return false;
|
||||
|
||||
return fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0;
|
||||
}
|
||||
|
||||
static void set_game_lifecycle_start_result(bool success) {
|
||||
pthread_mutex_lock(&g_game_lifecycle_start_mutex);
|
||||
g_game_lifecycle_start_success = success;
|
||||
g_game_lifecycle_start_ready = true;
|
||||
pthread_cond_broadcast(&g_game_lifecycle_start_cond);
|
||||
pthread_mutex_unlock(&g_game_lifecycle_start_mutex);
|
||||
}
|
||||
|
||||
static pending_game_launch_t *find_pending_game_launch(pid_t pid) {
|
||||
for (size_t i = 0; i < MAX_PENDING_GAME_EXEC_CANDIDATES; ++i) {
|
||||
pending_game_launch_t *entry = &g_pending_game_launches[i];
|
||||
if (entry->active && entry->pid == pid)
|
||||
return entry;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static pending_game_launch_t *reserve_pending_game_launch(uint64_t now_us) {
|
||||
pending_game_launch_t *oldest_entry = NULL;
|
||||
|
||||
for (size_t i = 0; i < MAX_PENDING_GAME_EXEC_CANDIDATES; ++i) {
|
||||
pending_game_launch_t *entry = &g_pending_game_launches[i];
|
||||
if (!entry->active)
|
||||
return entry;
|
||||
|
||||
if (now_us != 0 && entry->deadline_us != 0 && now_us >= entry->deadline_us)
|
||||
return entry;
|
||||
|
||||
if (!oldest_entry ||
|
||||
(entry->deadline_us != 0 &&
|
||||
(oldest_entry->deadline_us == 0 ||
|
||||
entry->deadline_us < oldest_entry->deadline_us))) {
|
||||
oldest_entry = entry;
|
||||
}
|
||||
}
|
||||
|
||||
return oldest_entry;
|
||||
}
|
||||
|
||||
static pending_game_launch_t *queue_pending_game_launch(pid_t pid,
|
||||
uint64_t exec_time_us,
|
||||
uint64_t now_us) {
|
||||
pending_game_launch_t *entry = find_pending_game_launch(pid);
|
||||
if (!entry)
|
||||
entry = reserve_pending_game_launch(now_us);
|
||||
|
||||
if (entry->active && entry->pid != pid) {
|
||||
log_debug(" [GAME] replacing pending exec pid=%ld with pid=%ld",
|
||||
(long)entry->pid, (long)pid);
|
||||
}
|
||||
|
||||
uint64_t base_time_us = exec_time_us != 0 ? exec_time_us : now_us;
|
||||
entry->active = true;
|
||||
entry->pid = pid;
|
||||
entry->exec_time_us = base_time_us;
|
||||
entry->deadline_us =
|
||||
base_time_us == 0 ? 0 : base_time_us + GAME_APPINFO_LOOKUP_TIMEOUT_US;
|
||||
return entry;
|
||||
}
|
||||
|
||||
static void defer_confirmed_game_launch_retry(pid_t pid, uint64_t exec_time_us,
|
||||
uint64_t now_us,
|
||||
const char *title_id) {
|
||||
clear_all_pending_game_launches();
|
||||
queue_pending_game_launch(pid, exec_time_us, now_us);
|
||||
|
||||
log_debug(" [GAME] deferring launch retry for %s pid=%ld", title_id,
|
||||
(long)pid);
|
||||
}
|
||||
|
||||
static uint64_t next_pending_game_wake_us(uint64_t now_us) {
|
||||
uint64_t next_wake_us = 0;
|
||||
|
||||
for (size_t i = 0; i < MAX_PENDING_GAME_EXEC_CANDIDATES; ++i) {
|
||||
pending_game_launch_t *entry = &g_pending_game_launches[i];
|
||||
if (!entry->active)
|
||||
continue;
|
||||
|
||||
if (now_us == 0) {
|
||||
next_wake_us = min_nonzero_u64(next_wake_us, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
uint64_t poll_wake_us = now_us + GAME_LIFECYCLE_POLL_INTERVAL_US;
|
||||
uint64_t entry_wake_us = min_nonzero_u64(entry->deadline_us, poll_wake_us);
|
||||
next_wake_us = min_nonzero_u64(next_wake_us, entry_wake_us);
|
||||
}
|
||||
|
||||
return next_wake_us;
|
||||
}
|
||||
|
||||
static const struct timespec *compute_game_wait_timeout(
|
||||
struct timespec *timeout_out) {
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
uint64_t next_wake_us = 0;
|
||||
|
||||
next_wake_us = min_nonzero_u64(next_wake_us, next_pending_game_wake_us(now_us));
|
||||
next_wake_us = min_nonzero_u64(next_wake_us, sm_kstuff_game_next_wake_us(now_us));
|
||||
next_wake_us = min_nonzero_u64(next_wake_us, sm_mdbg_next_wake_us(now_us));
|
||||
if (next_wake_us == 0)
|
||||
return NULL;
|
||||
|
||||
uint64_t wait_us = GAME_LIFECYCLE_POLL_INTERVAL_US;
|
||||
if (now_us != 0 && next_wake_us > now_us)
|
||||
wait_us = next_wake_us - now_us;
|
||||
else if (now_us != 0)
|
||||
wait_us = 0;
|
||||
|
||||
timeout_out->tv_sec = (time_t)(wait_us / 1000000ull);
|
||||
timeout_out->tv_nsec = (long)((wait_us % 1000000ull) * 1000ull);
|
||||
return timeout_out;
|
||||
}
|
||||
|
||||
static void poll_game_modules(int kq) {
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
for (size_t i = 0; i < MAX_PENDING_GAME_EXEC_CANDIDATES; ++i) {
|
||||
pending_game_launch_t *entry = &g_pending_game_launches[i];
|
||||
if (!entry->active)
|
||||
continue;
|
||||
|
||||
char title_id[MAX_TITLE_ID];
|
||||
uint32_t app_id = 0;
|
||||
if (resolve_game_title_id(entry->pid, title_id, &app_id)) {
|
||||
pid_t pid = entry->pid;
|
||||
uint64_t exec_time_us = entry->exec_time_us;
|
||||
if (is_supported_title_id(title_id)) {
|
||||
if (dispatch_game_launch(kq, pid, exec_time_us, title_id, app_id)) {
|
||||
clear_pending_game_launch(entry);
|
||||
clear_all_pending_game_launches();
|
||||
break;
|
||||
}
|
||||
defer_confirmed_game_launch_retry(pid, exec_time_us, now_us, title_id);
|
||||
break;
|
||||
} else {
|
||||
clear_pending_game_launch(entry);
|
||||
}
|
||||
} else if (!is_process_alive(entry->pid)) {
|
||||
clear_pending_game_launch(entry);
|
||||
}
|
||||
|
||||
if (entry->active && now_us != 0 && entry->deadline_us != 0 &&
|
||||
now_us >= entry->deadline_us) {
|
||||
log_debug(" [GAME] title_id was not available within %uus for pid=%ld",
|
||||
(unsigned)GAME_APPINFO_LOOKUP_TIMEOUT_US, (long)entry->pid);
|
||||
clear_pending_game_launch(entry);
|
||||
}
|
||||
}
|
||||
|
||||
sm_kstuff_game_poll();
|
||||
sm_mdbg_poll();
|
||||
}
|
||||
|
||||
static bool register_game_exit_watch(int kq, pid_t pid) {
|
||||
struct kevent kev;
|
||||
EV_SET(&kev, (uintptr_t)pid, EVFILT_PROC, EV_ADD | EV_ENABLE | EV_CLEAR,
|
||||
NOTE_EXIT, 0, NULL);
|
||||
if (kevent(kq, &kev, 1, NULL, 0, NULL) != 0) {
|
||||
log_debug(" [GAME] failed to register exit watch for pid=%ld: %s",
|
||||
(long)pid, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void handle_game_exec(int kq, pid_t pid) {
|
||||
if (find_pending_game_launch(pid))
|
||||
return;
|
||||
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
char title_id[MAX_TITLE_ID];
|
||||
uint32_t app_id = 0;
|
||||
if (resolve_game_title_id(pid, title_id, &app_id)) {
|
||||
if (is_supported_title_id(title_id)) {
|
||||
if (dispatch_game_launch(kq, pid, now_us, title_id, app_id))
|
||||
clear_all_pending_game_launches();
|
||||
else
|
||||
defer_confirmed_game_launch_retry(pid, now_us, now_us, title_id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_process_alive(pid))
|
||||
return;
|
||||
|
||||
queue_pending_game_launch(pid, now_us, now_us);
|
||||
}
|
||||
|
||||
static void handle_game_exit(pid_t pid) {
|
||||
char title_id[MAX_TITLE_ID] = {0};
|
||||
bool had_active_title = consume_active_game_title(pid, title_id);
|
||||
pending_game_launch_t *entry = find_pending_game_launch(pid);
|
||||
if (entry)
|
||||
clear_pending_game_launch(entry);
|
||||
if (atomic_load(&g_active_game_pid) == pid)
|
||||
publish_active_game_pid(0);
|
||||
sm_fakelib_game_on_exit(pid);
|
||||
sm_kstuff_game_on_exit(pid);
|
||||
if (had_active_title) {
|
||||
int snd0_updates = normalize_snd0info_for_title(title_id);
|
||||
if (snd0_updates >= 0)
|
||||
log_debug(" [DB] snd0info normalized after game exit rows=%d title=%s",
|
||||
snd0_updates, title_id);
|
||||
}
|
||||
}
|
||||
|
||||
static void restore_suspended_game_if_alive(int kq, pid_t pid) {
|
||||
if (pid <= 0 || !is_process_alive(pid))
|
||||
return;
|
||||
|
||||
char title_id[MAX_TITLE_ID];
|
||||
uint32_t app_id = 0;
|
||||
if (!resolve_game_title_id(pid, title_id, &app_id))
|
||||
return;
|
||||
if (!is_supported_title_id(title_id))
|
||||
return;
|
||||
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
log_debug(" [GAME] resumed: %s pid=%ld app_id=0x%08X", title_id,
|
||||
(long)pid, app_id);
|
||||
(void)dispatch_game_launch(kq, pid, now_us, title_id, app_id);
|
||||
}
|
||||
|
||||
static void *game_lifecycle_watcher_main(void *arg) {
|
||||
(void)arg;
|
||||
|
||||
pid_t syscore_pid = find_pid_by_name("SceSysCore.elf", false);
|
||||
if (syscore_pid <= 0) {
|
||||
log_debug(" [GAME] failed to find SceSysCore.elf");
|
||||
set_game_lifecycle_start_result(false);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int kq = kqueue();
|
||||
if (kq < 0) {
|
||||
log_debug(" [GAME] kqueue failed: %s", strerror(errno));
|
||||
set_game_lifecycle_start_result(false);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct kevent kev;
|
||||
EV_SET(&kev, (uintptr_t)syscore_pid, EVFILT_PROC, EV_ADD | EV_ENABLE | EV_CLEAR,
|
||||
NOTE_FORK | NOTE_EXEC | NOTE_TRACK, 0, NULL);
|
||||
if (kevent(kq, &kev, 1, NULL, 0, NULL) != 0) {
|
||||
log_debug(" [GAME] proc watch registration failed: %s",
|
||||
strerror(errno));
|
||||
close(kq);
|
||||
set_game_lifecycle_start_result(false);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
EV_SET(&kev, (uintptr_t)g_game_lifecycle_wake_pipe[0], EVFILT_READ,
|
||||
EV_ADD | EV_ENABLE | EV_CLEAR, 0, 0, NULL);
|
||||
if (kevent(kq, &kev, 1, NULL, 0, NULL) != 0) {
|
||||
log_debug(" [GAME] wake pipe registration failed: %s", strerror(errno));
|
||||
close(kq);
|
||||
set_game_lifecycle_start_result(false);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
set_game_lifecycle_start_result(true);
|
||||
log_debug(" [GAME] lifecycle watcher started");
|
||||
|
||||
bool sleep_cleanup_done = false;
|
||||
pid_t suspended_game_pid = 0;
|
||||
while (!g_game_lifecycle_stop_requested && !should_stop_requested()) {
|
||||
if (runtime_sleep_mode_active()) {
|
||||
if (!sleep_cleanup_done) {
|
||||
suspended_game_pid = atomic_load(&g_active_game_pid);
|
||||
clear_all_pending_game_launches();
|
||||
sm_fakelib_game_shutdown();
|
||||
sm_kstuff_sleep_enter();
|
||||
publish_active_game(0, NULL);
|
||||
sleep_cleanup_done = true;
|
||||
}
|
||||
|
||||
fd_set readfds;
|
||||
FD_ZERO(&readfds);
|
||||
FD_SET(g_game_lifecycle_wake_pipe[0], &readfds);
|
||||
int rc = select(g_game_lifecycle_wake_pipe[0] + 1, &readfds, NULL, NULL,
|
||||
NULL);
|
||||
if (rc < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
log_debug(" [GAME] sleep wait failed: %s", strerror(errno));
|
||||
break;
|
||||
}
|
||||
drain_game_lifecycle_wake_pipe();
|
||||
continue;
|
||||
}
|
||||
if (sleep_cleanup_done) {
|
||||
if (!discard_game_lifecycle_events_nowait(kq))
|
||||
break;
|
||||
sleep_cleanup_done = false;
|
||||
restore_suspended_game_if_alive(kq, suspended_game_pid);
|
||||
sm_kstuff_sleep_leave();
|
||||
suspended_game_pid = 0;
|
||||
}
|
||||
|
||||
struct kevent event;
|
||||
struct timespec timeout;
|
||||
const struct timespec *timeout_ptr = compute_game_wait_timeout(&timeout);
|
||||
int nev = kevent(kq, NULL, 0, &event, 1, timeout_ptr);
|
||||
if (nev < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
if (g_game_lifecycle_stop_requested)
|
||||
break;
|
||||
log_debug(" [GAME] kevent wait failed: %s", strerror(errno));
|
||||
break;
|
||||
}
|
||||
if (nev > 0) {
|
||||
if (event.filter == EVFILT_READ &&
|
||||
event.ident == (uintptr_t)g_game_lifecycle_wake_pipe[0]) {
|
||||
drain_game_lifecycle_wake_pipe();
|
||||
} else if ((event.fflags & NOTE_TRACKERR) != 0) {
|
||||
log_debug(" [GAME] NOTE_TRACKERR for pid=%ld", (long)event.ident);
|
||||
} else {
|
||||
if ((event.fflags & NOTE_EXEC) != 0)
|
||||
handle_game_exec(kq, (pid_t)event.ident);
|
||||
if ((event.fflags & NOTE_EXIT) != 0)
|
||||
handle_game_exit((pid_t)event.ident);
|
||||
}
|
||||
}
|
||||
|
||||
if (runtime_sleep_mode_active())
|
||||
continue;
|
||||
poll_game_modules(kq);
|
||||
}
|
||||
|
||||
clear_all_pending_game_launches();
|
||||
sm_fakelib_game_shutdown();
|
||||
sm_kstuff_game_shutdown();
|
||||
publish_active_game(0, NULL);
|
||||
close(kq);
|
||||
log_debug(" [GAME] lifecycle watcher stopped");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool start_game_lifecycle_watcher(void) {
|
||||
if (g_game_lifecycle_thread_started)
|
||||
return true;
|
||||
if (pipe(g_game_lifecycle_wake_pipe) != 0) {
|
||||
log_debug(" [GAME] wake pipe creation failed: %s", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (!set_fd_nonblocking(g_game_lifecycle_wake_pipe[0]) ||
|
||||
!set_fd_nonblocking(g_game_lifecycle_wake_pipe[1])) {
|
||||
log_debug(" [GAME] wake pipe nonblocking setup failed: %s",
|
||||
strerror(errno));
|
||||
close_game_lifecycle_wake_pipe();
|
||||
return false;
|
||||
}
|
||||
|
||||
g_game_lifecycle_stop_requested = 0;
|
||||
pthread_mutex_lock(&g_game_lifecycle_start_mutex);
|
||||
g_game_lifecycle_start_ready = false;
|
||||
g_game_lifecycle_start_success = false;
|
||||
pthread_mutex_unlock(&g_game_lifecycle_start_mutex);
|
||||
|
||||
int rc =
|
||||
pthread_create(&g_game_lifecycle_thread, NULL, game_lifecycle_watcher_main,
|
||||
NULL);
|
||||
if (rc != 0) {
|
||||
log_debug(" [GAME] watcher start failed: %s", strerror(rc));
|
||||
close_game_lifecycle_wake_pipe();
|
||||
return false;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_game_lifecycle_start_mutex);
|
||||
while (!g_game_lifecycle_start_ready)
|
||||
pthread_cond_wait(&g_game_lifecycle_start_cond, &g_game_lifecycle_start_mutex);
|
||||
bool start_success = g_game_lifecycle_start_success;
|
||||
pthread_mutex_unlock(&g_game_lifecycle_start_mutex);
|
||||
|
||||
if (!start_success) {
|
||||
(void)pthread_join(g_game_lifecycle_thread, NULL);
|
||||
close_game_lifecycle_wake_pipe();
|
||||
return false;
|
||||
}
|
||||
|
||||
g_game_lifecycle_thread_started = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void wake_game_lifecycle_watcher(void) {
|
||||
if (g_game_lifecycle_wake_pipe[1] < 0)
|
||||
return;
|
||||
|
||||
char wake = 'x';
|
||||
ssize_t written = write(g_game_lifecycle_wake_pipe[1], &wake, sizeof(wake));
|
||||
if (written < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
|
||||
log_debug(" [GAME] wake pipe write failed: %s", strerror(errno));
|
||||
}
|
||||
}
|
||||
|
||||
void stop_game_lifecycle_watcher(void) {
|
||||
if (!g_game_lifecycle_thread_started)
|
||||
return;
|
||||
|
||||
g_game_lifecycle_stop_requested = 1;
|
||||
wake_game_lifecycle_watcher();
|
||||
|
||||
(void)pthread_join(g_game_lifecycle_thread, NULL);
|
||||
g_game_lifecycle_thread_started = false;
|
||||
g_game_lifecycle_stop_requested = 0;
|
||||
publish_active_game(0, NULL);
|
||||
close_game_lifecycle_wake_pipe();
|
||||
}
|
||||
|
||||
bool sm_game_lifecycle_has_active_game(void) {
|
||||
return atomic_load(&g_active_game_pid) > 0;
|
||||
}
|
||||
|
||||
bool refresh_game_lifecycle_watcher(void) {
|
||||
if (!g_game_lifecycle_thread_started)
|
||||
return start_game_lifecycle_watcher();
|
||||
|
||||
wake_game_lifecycle_watcher();
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
#include "sm_platform.h"
|
||||
#include "sm_gameinfo.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_path_state.h"
|
||||
|
||||
// --- Game Metadata Parsing (param.json) ---
|
||||
static int extract_json_string(const char *json, const char *key, char *out,
|
||||
size_t out_size) {
|
||||
char search[64];
|
||||
snprintf(search, sizeof(search), "\"%s\"", key);
|
||||
const char *p = strstr(json, search);
|
||||
if (!p)
|
||||
return -1;
|
||||
p = strchr(p + strlen(search), ':');
|
||||
if (!p)
|
||||
return -2;
|
||||
while (*++p && isspace(*p)) {
|
||||
/* skip */
|
||||
}
|
||||
if (*p != '"')
|
||||
return -3;
|
||||
p++;
|
||||
|
||||
size_t i = 0;
|
||||
while (i < out_size - 1 && p[i] && p[i] != '"') {
|
||||
out[i] = p[i];
|
||||
i++;
|
||||
}
|
||||
out[i] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool get_game_info(const char *base_path, const struct stat *param_st,
|
||||
char *out_id, char *out_name) {
|
||||
out_id[0] = '\0';
|
||||
out_name[0] = '\0';
|
||||
if (!S_ISREG(param_st->st_mode))
|
||||
return false;
|
||||
|
||||
bool cached_valid = false;
|
||||
if (load_cached_game_info(base_path, param_st, out_id, out_name, &cached_valid))
|
||||
return cached_valid;
|
||||
|
||||
if (param_st->st_size <= 0 || param_st->st_size > MAX_PARAM_JSON_SIZE) {
|
||||
store_cached_game_info(base_path, param_st, false, "", "");
|
||||
return false;
|
||||
}
|
||||
|
||||
char path[MAX_PATH];
|
||||
int written = snprintf(path, sizeof(path), "%s/sce_sys/param.json",
|
||||
base_path);
|
||||
if (written < 0 || (size_t)written >= sizeof(path)) {
|
||||
store_cached_game_info(base_path, param_st, false, "", "");
|
||||
return false;
|
||||
}
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) {
|
||||
store_cached_game_info(base_path, param_st, false, "", "");
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t len = (size_t)param_st->st_size;
|
||||
char *buf = (char *)malloc(len + 1);
|
||||
if (!buf) {
|
||||
fclose(f);
|
||||
return false;
|
||||
}
|
||||
bool read_ok = (fread(buf, 1, len, f) == len);
|
||||
fclose(f);
|
||||
if (!read_ok) {
|
||||
free(buf);
|
||||
return false;
|
||||
}
|
||||
buf[len] = '\0';
|
||||
|
||||
bool valid = false;
|
||||
int res = extract_json_string(buf, "titleId", out_id, MAX_TITLE_ID);
|
||||
if (res != 0)
|
||||
res = extract_json_string(buf, "title_id", out_id, MAX_TITLE_ID);
|
||||
if (res == 0) {
|
||||
const char *en_ptr = strstr(buf, "\"en-US\"");
|
||||
const char *search_start = en_ptr ? en_ptr : buf;
|
||||
if (extract_json_string(search_start, "titleName", out_name,
|
||||
MAX_TITLE_NAME) != 0)
|
||||
extract_json_string(buf, "titleName", out_name, MAX_TITLE_NAME);
|
||||
if (out_name[0] == '\0')
|
||||
(void)strlcpy(out_name, out_id, MAX_TITLE_NAME);
|
||||
valid = true;
|
||||
}
|
||||
free(buf);
|
||||
|
||||
store_cached_game_info(base_path, param_st, valid, out_id, out_name);
|
||||
return valid;
|
||||
}
|
||||
|
||||
bool directory_has_param_json(const char *dir_path, struct stat *param_st_out) {
|
||||
int dir_fd = open(dir_path, O_RDONLY | O_DIRECTORY);
|
||||
if (dir_fd < 0)
|
||||
return false;
|
||||
|
||||
struct stat st;
|
||||
if (fstatat(dir_fd, "sce_sys", &st, 0) == 0 && S_ISDIR(st.st_mode) &&
|
||||
fstatat(dir_fd, "sce_sys/param.json", &st, 0) == 0 &&
|
||||
S_ISREG(st.st_mode)) {
|
||||
if (param_st_out)
|
||||
*param_st_out = st;
|
||||
close(dir_fd);
|
||||
return true;
|
||||
}
|
||||
|
||||
close(dir_fd);
|
||||
return false;
|
||||
}
|
||||
+1382
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,142 @@
|
||||
#include "sm_platform.h"
|
||||
#include <pthread.h>
|
||||
|
||||
#include "sm_image_cache.h"
|
||||
#include "sm_limits.h"
|
||||
|
||||
struct ImageCache {
|
||||
char path[MAX_PATH];
|
||||
char mount_point[MAX_PATH];
|
||||
int unit_id;
|
||||
attach_backend_t backend;
|
||||
bool valid;
|
||||
};
|
||||
|
||||
static struct ImageCache g_image_cache[MAX_IMAGE_MOUNTS];
|
||||
static pthread_mutex_t g_image_cache_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static int find_cache_index(const char *path, const char *mount_point) {
|
||||
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
|
||||
if (!g_image_cache[k].valid)
|
||||
continue;
|
||||
if (mount_point && strcmp(g_image_cache[k].mount_point, mount_point) == 0) {
|
||||
return k;
|
||||
}
|
||||
if (path && strcmp(g_image_cache[k].path, path) == 0)
|
||||
return k;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int upsert_image_source_mapping(const char *path, const char *mount_point) {
|
||||
int entry_index = find_cache_index(path, mount_point);
|
||||
if (entry_index >= 0) {
|
||||
struct ImageCache *entry = &g_image_cache[entry_index];
|
||||
(void)strlcpy(entry->path, path, sizeof(entry->path));
|
||||
(void)strlcpy(entry->mount_point, mount_point, sizeof(entry->mount_point));
|
||||
return entry_index;
|
||||
}
|
||||
|
||||
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
|
||||
if (!g_image_cache[k].valid) {
|
||||
(void)strlcpy(g_image_cache[k].path, path, sizeof(g_image_cache[k].path));
|
||||
(void)strlcpy(g_image_cache[k].mount_point, mount_point,
|
||||
sizeof(g_image_cache[k].mount_point));
|
||||
g_image_cache[k].unit_id = -1;
|
||||
g_image_cache[k].backend = ATTACH_BACKEND_NONE;
|
||||
g_image_cache[k].valid = true;
|
||||
return k;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool cache_image_source_mapping(const char *path, const char *mount_point) {
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
bool ok = upsert_image_source_mapping(path, mount_point) >= 0;
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool cache_image_mount(const char *path, const char *mount_point, int unit_id,
|
||||
attach_backend_t backend) {
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int entry_index = upsert_image_source_mapping(path, mount_point);
|
||||
if (entry_index < 0) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct ImageCache *entry = &g_image_cache[entry_index];
|
||||
entry->unit_id = unit_id;
|
||||
entry->backend = backend;
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool get_image_cache_entry(int index, image_cache_entry_t *entry_out) {
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
if (index < 0 || index >= MAX_IMAGE_MOUNTS || !g_image_cache[index].valid) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
memset(entry_out, 0, sizeof(*entry_out));
|
||||
(void)strlcpy(entry_out->path, g_image_cache[index].path,
|
||||
sizeof(entry_out->path));
|
||||
(void)strlcpy(entry_out->mount_point, g_image_cache[index].mount_point,
|
||||
sizeof(entry_out->mount_point));
|
||||
entry_out->unit_id = g_image_cache[index].unit_id;
|
||||
entry_out->backend = g_image_cache[index].backend;
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
void invalidate_image_cache_entry(int index) {
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
if (index < 0 || index >= MAX_IMAGE_MOUNTS) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return;
|
||||
}
|
||||
memset(&g_image_cache[index], 0, sizeof(g_image_cache[index]));
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
}
|
||||
|
||||
bool resolve_device_from_mount_cache(const char *mount_point,
|
||||
attach_backend_t *backend_out,
|
||||
int *unit_out) {
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int entry_index = find_cache_index(NULL, mount_point);
|
||||
if (entry_index < 0) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return false;
|
||||
}
|
||||
const struct ImageCache *entry = &g_image_cache[entry_index];
|
||||
if (entry->backend == ATTACH_BACKEND_NONE || entry->unit_id < 0) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return false;
|
||||
}
|
||||
*backend_out = entry->backend;
|
||||
*unit_out = entry->unit_id;
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool resolve_image_source_from_mount_cache(const char *mount_point,
|
||||
char *path_out,
|
||||
size_t path_out_size) {
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int entry_index = find_cache_index(NULL, mount_point);
|
||||
if (entry_index < 0) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return false;
|
||||
}
|
||||
const struct ImageCache *entry = &g_image_cache[entry_index];
|
||||
|
||||
path_out[0] = '\0';
|
||||
(void)strlcpy(path_out, entry->path, path_out_size);
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,462 @@
|
||||
#include "sm_platform.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_install.h"
|
||||
#include "sm_install_queue.h"
|
||||
#include "sm_types.h"
|
||||
#include "sm_game_cache.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_filesystem.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_appdb.h"
|
||||
#include "sm_title_state.h"
|
||||
#include "sm_image_cache.h"
|
||||
#include "sm_paths.h"
|
||||
#include "sm_manual.h"
|
||||
|
||||
#include <dlfcn.h>
|
||||
|
||||
typedef int (*app_install_title_dir_fn_t)(const char *title_id,
|
||||
const char *install_path,
|
||||
void *reserved);
|
||||
|
||||
static const char *const k_app_inst_util_sprx_path =
|
||||
"/system/common/lib/libSceAppInstUtil.sprx";
|
||||
static const char *const k_app_install_title_dir_symbol =
|
||||
"sceAppInstUtilAppInstallTitleDir";
|
||||
|
||||
static app_install_title_dir_fn_t g_app_install_title_dir_fn = NULL;
|
||||
static bool g_app_install_title_dir_resolve_attempted = false;
|
||||
|
||||
static app_install_title_dir_fn_t resolve_app_install_title_dir(void) {
|
||||
if (g_app_install_title_dir_fn)
|
||||
return g_app_install_title_dir_fn;
|
||||
if (g_app_install_title_dir_resolve_attempted)
|
||||
return NULL;
|
||||
if (sm_firmware_major_version() >= 12u) {
|
||||
g_app_install_title_dir_resolve_attempted = true;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
g_app_install_title_dir_resolve_attempted = true;
|
||||
void *app_inst_util_handle = dlopen(k_app_inst_util_sprx_path, RTLD_LAZY);
|
||||
if (!app_inst_util_handle) {
|
||||
const char *error = dlerror();
|
||||
log_debug(" [REG] dlopen(%s) failed: %s", k_app_inst_util_sprx_path,
|
||||
error ? error : "unknown");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
dlerror();
|
||||
g_app_install_title_dir_fn = (app_install_title_dir_fn_t)dlsym(
|
||||
app_inst_util_handle, k_app_install_title_dir_symbol);
|
||||
if (g_app_install_title_dir_fn)
|
||||
return g_app_install_title_dir_fn;
|
||||
|
||||
const char *error = dlerror();
|
||||
log_debug(" [REG] %s lookup in %s failed: %s",
|
||||
k_app_install_title_dir_symbol, k_app_inst_util_sprx_path,
|
||||
error ? error : "unknown");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static bool write_link_file(const char *path, const char *value) {
|
||||
FILE *f = fopen(path, "w");
|
||||
if (!f) {
|
||||
log_debug(" [LINK] open failed for %s: %s", path, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
int saved_errno = 0;
|
||||
if (fprintf(f, "%s", value) < 0)
|
||||
saved_errno = errno;
|
||||
if (fflush(f) != 0 && saved_errno == 0)
|
||||
saved_errno = errno;
|
||||
if (fclose(f) != 0 && saved_errno == 0)
|
||||
saved_errno = errno;
|
||||
|
||||
if (saved_errno != 0) {
|
||||
errno = saved_errno;
|
||||
log_debug(" [LINK] write failed for %s: %s", path, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool is_appmeta_file(const char *name) {
|
||||
if (!name)
|
||||
return false;
|
||||
if (strcasecmp(name, "param.json") == 0 || strcasecmp(name, "param.sfo") == 0)
|
||||
return true;
|
||||
|
||||
const char *ext = strrchr(name, '.');
|
||||
if (!ext)
|
||||
return false;
|
||||
|
||||
return (strcasecmp(ext, ".png") == 0 || strcasecmp(ext, ".dds") == 0 ||
|
||||
strcasecmp(ext, ".at9") == 0);
|
||||
}
|
||||
|
||||
static bool copy_sce_sys_to_appmeta(const char *src_sce_sys,
|
||||
const char *user_appmeta_dir) {
|
||||
DIR *d = opendir(src_sce_sys);
|
||||
if (!d)
|
||||
return false;
|
||||
|
||||
bool ok = true;
|
||||
struct dirent *e;
|
||||
char src_path[MAX_PATH];
|
||||
char dst_path[MAX_PATH];
|
||||
struct stat st;
|
||||
while ((e = readdir(d)) != NULL) {
|
||||
if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0)
|
||||
continue;
|
||||
if (!is_appmeta_file(e->d_name))
|
||||
continue;
|
||||
|
||||
snprintf(src_path, sizeof(src_path), "%s/%s", src_sce_sys, e->d_name);
|
||||
if (stat(src_path, &st) != 0 || !S_ISREG(st.st_mode))
|
||||
continue;
|
||||
|
||||
snprintf(dst_path, sizeof(dst_path), "%s/%s", user_appmeta_dir, e->d_name);
|
||||
if (copy_file(src_path, dst_path) != 0) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (closedir(d) != 0)
|
||||
ok = false;
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
static bool copy_optional_trophy_metadata_file(const char *src_sce_sys,
|
||||
const char *dst_base,
|
||||
const char *relative_path) {
|
||||
char src_path[MAX_PATH];
|
||||
char dst_path[MAX_PATH];
|
||||
|
||||
snprintf(src_path, sizeof(src_path), "%s/%s", src_sce_sys, relative_path);
|
||||
if (access(src_path, F_OK) != 0)
|
||||
return true;
|
||||
|
||||
snprintf(dst_path, sizeof(dst_path), "%s/%s", dst_base, relative_path);
|
||||
if (access(dst_path, F_OK) == 0)
|
||||
return true;
|
||||
|
||||
if (copy_file(src_path, dst_path) == 0)
|
||||
return true;
|
||||
|
||||
log_debug(" [COPY] Failed to copy trophy metadata: %s -> %s", src_path,
|
||||
dst_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool update_trophy_metadata(const char *title_id,
|
||||
const char *src_sce_sys) {
|
||||
char dst_base[MAX_PATH];
|
||||
char dst_dir[MAX_PATH];
|
||||
bool ok = true;
|
||||
|
||||
snprintf(dst_base, sizeof(dst_base), "/system_data/priv/appmeta/%s",
|
||||
title_id);
|
||||
mkdir("/system_data/priv/appmeta", 0755);
|
||||
mkdir(dst_base, 0755);
|
||||
|
||||
snprintf(dst_dir, sizeof(dst_dir), "%s/trophy2", dst_base);
|
||||
mkdir(dst_dir, 0755);
|
||||
snprintf(dst_dir, sizeof(dst_dir), "%s/uds", dst_base);
|
||||
mkdir(dst_dir, 0755);
|
||||
|
||||
if (!copy_optional_trophy_metadata_file(src_sce_sys, dst_base,
|
||||
"trophy2/npbind.dat"))
|
||||
ok = false;
|
||||
if (!copy_optional_trophy_metadata_file(src_sce_sys, dst_base,
|
||||
"uds/npbind.dat"))
|
||||
ok = false;
|
||||
if (!copy_optional_trophy_metadata_file(src_sce_sys, dst_base, "param.json"))
|
||||
ok = false;
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
// --- Install/Remount Action ---
|
||||
static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
const char *title_name, bool is_remount,
|
||||
bool should_register,
|
||||
bool use_app_install_all,
|
||||
bool *has_src_snd0_out) {
|
||||
char user_appmeta_dir[MAX_PATH];
|
||||
char user_app_dir[MAX_PATH];
|
||||
char user_sce_sys[MAX_PATH];
|
||||
char src_sce_sys[MAX_PATH];
|
||||
char src_snd0[MAX_PATH];
|
||||
char image_source_path[MAX_PATH];
|
||||
bool has_image_source = false;
|
||||
bool appmeta_missing = false;
|
||||
bool metadata_restaged = false;
|
||||
bool restage_staging = false;
|
||||
bool restage_appmeta = false;
|
||||
bool has_src_snd0 = false;
|
||||
|
||||
snprintf(user_appmeta_dir, sizeof(user_appmeta_dir), "%s/%s", APPMETA_BASE,
|
||||
title_id);
|
||||
snprintf(user_app_dir, sizeof(user_app_dir), "%s/%s", APP_BASE, title_id);
|
||||
|
||||
if (is_under_image_mount_base(src_path)) {
|
||||
has_image_source = resolve_image_source_from_mount_cache(
|
||||
src_path, image_source_path, sizeof(image_source_path));
|
||||
if (!has_image_source) {
|
||||
log_debug(" [LINK] image source lookup failed for %s: %s", title_id,
|
||||
src_path);
|
||||
}
|
||||
}
|
||||
|
||||
snprintf(src_sce_sys, sizeof(src_sce_sys), "%s/sce_sys", src_path);
|
||||
appmeta_missing = !has_appmeta_data(title_id);
|
||||
if (is_remount && appmeta_missing) {
|
||||
log_debug(" [REG] appmeta missing, restaging metadata only: %s", title_id);
|
||||
}
|
||||
|
||||
restage_staging = (!is_remount || should_register);
|
||||
restage_appmeta = (!is_remount || appmeta_missing);
|
||||
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
return false;
|
||||
|
||||
// COPY FILES
|
||||
snprintf(src_snd0, sizeof(src_snd0), "%s/snd0.at9", src_sce_sys);
|
||||
has_src_snd0 = path_exists(src_snd0);
|
||||
if (!restage_staging && !restage_appmeta)
|
||||
log_debug(" [SPEED] Skipping file copy (Assets already exist)");
|
||||
|
||||
if (restage_staging) {
|
||||
mkdir(APP_BASE, 0777);
|
||||
mkdir(user_app_dir, 0777);
|
||||
snprintf(user_sce_sys, sizeof(user_sce_sys), "%s/sce_sys", user_app_dir);
|
||||
mkdir(user_sce_sys, 0777);
|
||||
if (copy_dir(src_sce_sys, user_sce_sys) != 0) {
|
||||
log_debug(" [COPY] Failed to copy sce_sys staging: %s -> %s", src_sce_sys,
|
||||
user_sce_sys);
|
||||
return false;
|
||||
}
|
||||
|
||||
char icon_src[MAX_PATH];
|
||||
char icon_dst[MAX_PATH];
|
||||
snprintf(icon_src, sizeof(icon_src), "%s/icon0.png", src_sce_sys);
|
||||
snprintf(icon_dst, sizeof(icon_dst), "%s/icon0.png", user_app_dir);
|
||||
if (copy_file(icon_src, icon_dst) != 0) {
|
||||
log_debug(" [COPY] Failed to copy staged icon: %s -> %s", icon_src,
|
||||
icon_dst);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (restage_appmeta) {
|
||||
mkdir(APPMETA_BASE, 0777);
|
||||
mkdir(user_appmeta_dir, 0777);
|
||||
if (!copy_sce_sys_to_appmeta(src_sce_sys, user_appmeta_dir)) {
|
||||
log_debug(" [COPY] Failed to copy appmeta files: %s -> %s", src_sce_sys,
|
||||
user_appmeta_dir);
|
||||
return false;
|
||||
}
|
||||
metadata_restaged = true;
|
||||
}
|
||||
|
||||
if (!update_trophy_metadata(title_id, src_sce_sys))
|
||||
return false;
|
||||
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
return false;
|
||||
|
||||
if (!mount_title_nullfs(title_id, src_path)) {
|
||||
log_debug(" [LINK] nullfs mount failed: title=%s src=%s", title_id,
|
||||
src_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
// WRITE TRACKER
|
||||
char lnk_path[MAX_PATH];
|
||||
runtime_mount_state_lock();
|
||||
if (runtime_sleep_mode_active()) {
|
||||
(void)rollback_title_nullfs_mount(title_id, src_path);
|
||||
runtime_mount_state_unlock();
|
||||
return false;
|
||||
}
|
||||
|
||||
mkdir(APP_BASE, 0777);
|
||||
mkdir(user_app_dir, 0777);
|
||||
snprintf(lnk_path, sizeof(lnk_path), "%s/mount.lnk", user_app_dir);
|
||||
if (!write_link_file(lnk_path, src_path)) {
|
||||
(void)rollback_title_nullfs_mount(title_id, src_path);
|
||||
runtime_mount_state_unlock();
|
||||
return false;
|
||||
}
|
||||
|
||||
log_debug(" [LINK] mount.lnk created: %s -> %s", lnk_path, src_path);
|
||||
|
||||
char img_lnk_path[MAX_PATH];
|
||||
snprintf(img_lnk_path, sizeof(img_lnk_path), "%s/mount_img.lnk",
|
||||
user_app_dir);
|
||||
if (has_image_source) {
|
||||
if (!write_link_file(img_lnk_path, image_source_path)) {
|
||||
(void)unlink(lnk_path);
|
||||
(void)rollback_title_nullfs_mount(title_id, src_path);
|
||||
runtime_mount_state_unlock();
|
||||
return false;
|
||||
}
|
||||
log_debug(" [LINK] mount_img.lnk created: %s -> %s", img_lnk_path,
|
||||
image_source_path);
|
||||
if (!cache_image_source_mapping(image_source_path, src_path)) {
|
||||
log_debug(" [LINK] image source cache update failed: %s -> %s",
|
||||
src_path, image_source_path);
|
||||
}
|
||||
} else if (unlink(img_lnk_path) != 0 && errno != ENOENT) {
|
||||
log_debug(" [LINK] remove failed for %s: %s", img_lnk_path,
|
||||
strerror(errno));
|
||||
}
|
||||
bool sleep_started = runtime_sleep_mode_active();
|
||||
runtime_mount_state_unlock();
|
||||
if (sleep_started || runtime_sleep_mode_active())
|
||||
return true;
|
||||
|
||||
if (!should_register) {
|
||||
if (metadata_restaged && has_src_snd0) {
|
||||
int snd0_updates = update_snd0info(title_id);
|
||||
if (snd0_updates >= 0)
|
||||
log_debug(" [DB] snd0info force-updated after appmeta refresh rows=%d",
|
||||
snd0_updates);
|
||||
}
|
||||
log_debug(" [REG] Skip (already present in app.db)");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (use_app_install_all) {
|
||||
if (has_src_snd0_out)
|
||||
*has_src_snd0_out = has_src_snd0;
|
||||
log_debug(" [REG] Prepared for batch install: %s (%s)", title_name,
|
||||
title_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
// REGISTER
|
||||
app_install_title_dir_fn_t app_install_title_dir_fn =
|
||||
resolve_app_install_title_dir();
|
||||
if (!app_install_title_dir_fn) {
|
||||
log_debug(" [REG] sceAppInstUtilAppInstallTitleDir unavailable");
|
||||
notify_system("Register failed: %s (%s)\nAppInstallTitleDir unavailable",
|
||||
title_name, title_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
mark_register_attempted(title_id);
|
||||
int res = app_install_title_dir_fn(title_id, APP_BASE "/", 0);
|
||||
sceKernelUsleep(200000);
|
||||
|
||||
if (res == 0) {
|
||||
invalidate_app_db_title_cache();
|
||||
clear_register_attempts(title_id);
|
||||
log_debug(" [REG] Installed NEW!");
|
||||
notify_game_installed_rich(title_id);
|
||||
if (has_src_snd0) {
|
||||
int snd0_updates = update_snd0info(title_id);
|
||||
if (snd0_updates >= 0)
|
||||
log_debug(" [DB] snd0info updated rows=%d", snd0_updates);
|
||||
}
|
||||
} else if ((uint32_t)res == 0x80990002u) {
|
||||
invalidate_app_db_title_cache();
|
||||
clear_register_attempts(title_id);
|
||||
log_debug(" [REG] Restored.");
|
||||
if (has_src_snd0) {
|
||||
int snd0_updates = update_snd0info(title_id);
|
||||
if (snd0_updates >= 0)
|
||||
log_debug(" [DB] snd0info updated rows=%d", snd0_updates);
|
||||
}
|
||||
// Silent on restore/remount to avoid spam
|
||||
} else {
|
||||
log_debug(" [REG] FAIL: 0x%x", res);
|
||||
notify_system("Register failed: %s (%s)\ncode=0x%08X", title_name, title_id,
|
||||
(uint32_t)res);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Execution (per discovered candidate) ---
|
||||
void process_scan_candidates(const scan_candidate_t *candidates,
|
||||
int candidate_count) {
|
||||
if (runtime_sleep_mode_active())
|
||||
return;
|
||||
|
||||
sm_install_poll_pending();
|
||||
|
||||
for (int i = 0; i < candidate_count; i++) {
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
return;
|
||||
|
||||
const scan_candidate_t *c = &candidates[i];
|
||||
bool has_src_snd0 = false;
|
||||
bool use_app_install_all =
|
||||
runtime_config()->app_install_all_enabled && !c->in_app_db;
|
||||
if (c->installed) {
|
||||
log_debug(" [ACTION] Remounting: %s", c->title_name);
|
||||
} else {
|
||||
log_debug(" [ACTION] Installing: %s (%s)", c->title_name, c->title_id);
|
||||
if (!use_app_install_all) {
|
||||
notify_system_info("Installing: %s (%s)...", c->title_name,
|
||||
c->title_id);
|
||||
}
|
||||
}
|
||||
|
||||
bool mounted = mount_and_install(c->path, c->title_id, c->title_name,
|
||||
c->installed, !c->in_app_db,
|
||||
use_app_install_all, &has_src_snd0);
|
||||
if (runtime_sleep_mode_active())
|
||||
return;
|
||||
|
||||
if (mounted) {
|
||||
if (c->manual && !use_app_install_all) {
|
||||
sm_manual_note_installed(c->manual_source_path, c->title_id,
|
||||
c->title_name);
|
||||
}
|
||||
if (use_app_install_all) {
|
||||
if (!sm_install_queue_candidate(c, has_src_snd0)) {
|
||||
log_debug(" [REG] Failed to queue staged install: %s (%s)",
|
||||
c->title_name, c->title_id);
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
return;
|
||||
|
||||
uint8_t failed_attempts = bump_failed_mount_attempts(c->title_id);
|
||||
if (failed_attempts == MAX_FAILED_MOUNT_ATTEMPTS) {
|
||||
log_debug(" [RETRY] limit reached (%u/%u): %s (%s)",
|
||||
(unsigned)failed_attempts,
|
||||
(unsigned)MAX_FAILED_MOUNT_ATTEMPTS, c->title_name,
|
||||
c->title_id);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
clear_failed_mount_attempts(c->title_id);
|
||||
} else {
|
||||
clear_failed_mount_attempts(c->title_id);
|
||||
cache_game_entry(c->path, c->title_id, c->title_name);
|
||||
}
|
||||
} else {
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
return;
|
||||
|
||||
uint8_t failed_attempts = bump_failed_mount_attempts(c->title_id);
|
||||
if (failed_attempts == MAX_FAILED_MOUNT_ATTEMPTS) {
|
||||
log_debug(" [RETRY] limit reached (%u/%u): %s (%s)",
|
||||
(unsigned)failed_attempts,
|
||||
(unsigned)MAX_FAILED_MOUNT_ATTEMPTS, c->title_name,
|
||||
c->title_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!sm_install_submit_queued())
|
||||
sm_install_note_submit_failure();
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include "sm_install_queue.h"
|
||||
#include "sm_types.h"
|
||||
#include "sm_appdb.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_game_cache.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_manual.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_time.h"
|
||||
#include "sm_title_state.h"
|
||||
|
||||
#define INSTALL_WAIT_TIMEOUT_US (300ull * 1000000ull)
|
||||
#define INSTALL_POLL_INTERVAL_US (2ull * 1000000ull)
|
||||
#define INSTALL_SUBMIT_RETRY_US (2ull * 1000000ull)
|
||||
|
||||
typedef enum {
|
||||
INSTALL_TRACK_NONE = 0,
|
||||
INSTALL_TRACK_QUEUED,
|
||||
INSTALL_TRACK_SUBMITTED,
|
||||
} install_track_state_t;
|
||||
|
||||
typedef struct {
|
||||
char title_id[MAX_TITLE_ID];
|
||||
char title_name[MAX_TITLE_NAME];
|
||||
char source_path[MAX_PATH];
|
||||
char manual_source_path[MAX_PATH];
|
||||
uint64_t requested_at_us;
|
||||
bool has_src_snd0;
|
||||
bool manual;
|
||||
install_track_state_t state;
|
||||
} pending_install_entry_t;
|
||||
|
||||
static pending_install_entry_t g_pending_installs[MAX_PENDING];
|
||||
static uint64_t g_pending_install_poll_due_us = 0;
|
||||
static uint64_t g_queued_install_submit_due_us = 0;
|
||||
static int g_tracked_install_count = 0;
|
||||
static int g_submitted_install_count = 0;
|
||||
static bool g_queued_install_batch_announced = false;
|
||||
static bool g_queued_install_submit_failure_notified = false;
|
||||
|
||||
static int count_queued_installs(void);
|
||||
static uint8_t note_pending_install_failure(const pending_install_entry_t *entry);
|
||||
static void drop_queued_install_entry(pending_install_entry_t *entry);
|
||||
|
||||
static bool install_queue_active(void) {
|
||||
return runtime_config()->app_install_all_enabled || g_tracked_install_count > 0;
|
||||
}
|
||||
|
||||
static pending_install_entry_t *find_pending_install_entry(
|
||||
const char *title_id) {
|
||||
if (!title_id || title_id[0] == '\0')
|
||||
return NULL;
|
||||
|
||||
for (int i = 0; i < MAX_PENDING; i++) {
|
||||
if (g_pending_installs[i].state == INSTALL_TRACK_NONE)
|
||||
continue;
|
||||
if (strcmp(g_pending_installs[i].title_id, title_id) == 0)
|
||||
return &g_pending_installs[i];
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static pending_install_entry_t *reserve_pending_install_entry(
|
||||
const char *title_id) {
|
||||
pending_install_entry_t *entry = find_pending_install_entry(title_id);
|
||||
if (entry)
|
||||
return entry;
|
||||
|
||||
for (int i = 0; i < MAX_PENDING; i++) {
|
||||
if (g_pending_installs[i].state == INSTALL_TRACK_NONE)
|
||||
return &g_pending_installs[i];
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static uint64_t next_pending_install_timeout_us(void) {
|
||||
if (g_submitted_install_count <= 0)
|
||||
return 0;
|
||||
|
||||
uint64_t next_deadline = 0;
|
||||
for (int i = 0; i < MAX_PENDING; i++) {
|
||||
const pending_install_entry_t *entry = &g_pending_installs[i];
|
||||
if (entry->state != INSTALL_TRACK_SUBMITTED || entry->requested_at_us == 0)
|
||||
continue;
|
||||
|
||||
uint64_t deadline_us = entry->requested_at_us + INSTALL_WAIT_TIMEOUT_US;
|
||||
if (next_deadline == 0 || deadline_us < next_deadline)
|
||||
next_deadline = deadline_us;
|
||||
}
|
||||
|
||||
return next_deadline;
|
||||
}
|
||||
|
||||
static void schedule_pending_install_poll(uint64_t now_us) {
|
||||
if (g_submitted_install_count <= 0) {
|
||||
g_pending_install_poll_due_us = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
g_pending_install_poll_due_us =
|
||||
now_us == 0 ? 1 : now_us + INSTALL_POLL_INTERVAL_US;
|
||||
}
|
||||
|
||||
static void schedule_queued_install_submit(uint64_t now_us) {
|
||||
if (count_queued_installs() <= 0 || g_submitted_install_count > 0) {
|
||||
g_queued_install_submit_due_us = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
g_queued_install_submit_due_us = now_us == 0 ? 1 : now_us;
|
||||
}
|
||||
|
||||
static void schedule_queued_install_submit_retry(uint64_t now_us) {
|
||||
if (count_queued_installs() <= 0 || g_submitted_install_count > 0) {
|
||||
g_queued_install_submit_due_us = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
g_queued_install_submit_due_us =
|
||||
now_us == 0 ? 1 : now_us + INSTALL_SUBMIT_RETRY_US;
|
||||
}
|
||||
|
||||
static void clear_pending_install_entry(pending_install_entry_t *entry) {
|
||||
if (!entry)
|
||||
return;
|
||||
if (entry->state == INSTALL_TRACK_SUBMITTED && g_submitted_install_count > 0)
|
||||
g_submitted_install_count--;
|
||||
if (entry->state != INSTALL_TRACK_NONE && g_tracked_install_count > 0)
|
||||
g_tracked_install_count--;
|
||||
memset(entry, 0, sizeof(*entry));
|
||||
if (g_submitted_install_count <= 0)
|
||||
g_pending_install_poll_due_us = 0;
|
||||
if (g_tracked_install_count <= 0) {
|
||||
g_queued_install_submit_due_us = 0;
|
||||
g_queued_install_batch_announced = false;
|
||||
g_queued_install_submit_failure_notified = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_title_install_pending(const char *title_id) {
|
||||
return find_pending_install_entry(title_id) != NULL;
|
||||
}
|
||||
|
||||
void sm_install_forget_pending_title(const char *title_id) {
|
||||
pending_install_entry_t *entry = find_pending_install_entry(title_id);
|
||||
if (entry)
|
||||
clear_pending_install_entry(entry);
|
||||
}
|
||||
|
||||
static void append_batch_install_line(char *message, size_t message_size,
|
||||
const char *title_name,
|
||||
const char *title_id, int *shown_count) {
|
||||
if (!message || message_size == 0 || !title_name || !title_id || !shown_count)
|
||||
return;
|
||||
|
||||
size_t used = strlen(message);
|
||||
if (used >= message_size)
|
||||
return;
|
||||
|
||||
int written = snprintf(message + used, message_size - used, "\n%s (%s)",
|
||||
title_name, title_id);
|
||||
if (written < 0 || (size_t)written >= message_size - used)
|
||||
return;
|
||||
|
||||
(*shown_count)++;
|
||||
}
|
||||
|
||||
bool sm_install_queue_candidate(const scan_candidate_t *candidate,
|
||||
bool has_src_snd0) {
|
||||
if (!candidate || candidate->title_id[0] == '\0' ||
|
||||
candidate->title_name[0] == '\0' || candidate->path[0] == '\0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!runtime_config()->app_install_all_enabled)
|
||||
return false;
|
||||
|
||||
bool was_queue_empty = (count_queued_installs() <= 0);
|
||||
pending_install_entry_t *entry =
|
||||
reserve_pending_install_entry(candidate->title_id);
|
||||
if (!entry) {
|
||||
log_debug(" [REG] install queue full, cannot queue %s (%s)",
|
||||
candidate->title_name, candidate->title_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
install_track_state_t previous_state = entry->state;
|
||||
if (previous_state == INSTALL_TRACK_SUBMITTED)
|
||||
return true;
|
||||
|
||||
memset(entry, 0, sizeof(*entry));
|
||||
(void)strlcpy(entry->title_id, candidate->title_id, sizeof(entry->title_id));
|
||||
(void)strlcpy(entry->title_name, candidate->title_name,
|
||||
sizeof(entry->title_name));
|
||||
(void)strlcpy(entry->source_path, candidate->path, sizeof(entry->source_path));
|
||||
(void)strlcpy(entry->manual_source_path, candidate->manual_source_path,
|
||||
sizeof(entry->manual_source_path));
|
||||
entry->manual = candidate->manual;
|
||||
entry->has_src_snd0 = has_src_snd0;
|
||||
entry->state = INSTALL_TRACK_QUEUED;
|
||||
if (previous_state == INSTALL_TRACK_NONE)
|
||||
g_tracked_install_count++;
|
||||
if (was_queue_empty) {
|
||||
g_queued_install_batch_announced = false;
|
||||
g_queued_install_submit_failure_notified = false;
|
||||
}
|
||||
schedule_queued_install_submit(monotonic_time_us());
|
||||
return true;
|
||||
}
|
||||
|
||||
static void finalize_pending_install_success(pending_install_entry_t *entry) {
|
||||
if (!entry)
|
||||
return;
|
||||
|
||||
log_debug(" [REG] Installed: %s (%s)", entry->title_name, entry->title_id);
|
||||
notify_game_installed_rich(entry->title_id);
|
||||
if (entry->has_src_snd0) {
|
||||
int snd0_updates = update_snd0info(entry->title_id);
|
||||
if (snd0_updates >= 0)
|
||||
log_debug(" [DB] snd0info updated rows=%d", snd0_updates);
|
||||
}
|
||||
cache_game_entry(entry->source_path, entry->title_id, entry->title_name);
|
||||
if (entry->manual)
|
||||
sm_manual_note_installed(entry->manual_source_path, entry->title_id,
|
||||
entry->title_name);
|
||||
clear_register_attempts(entry->title_id);
|
||||
clear_failed_mount_attempts(entry->title_id);
|
||||
clear_pending_install_entry(entry);
|
||||
}
|
||||
|
||||
static void finalize_pending_install_timeout(pending_install_entry_t *entry) {
|
||||
if (!entry)
|
||||
return;
|
||||
|
||||
log_debug(" [REG] Install timeout after 5 minutes: %s (%s)",
|
||||
entry->title_name, entry->title_id);
|
||||
notify_system("Install failed: %s (%s)\nTimed out after 5 minutes",
|
||||
entry->title_name, entry->title_id);
|
||||
(void)note_pending_install_failure(entry);
|
||||
clear_pending_install_entry(entry);
|
||||
}
|
||||
|
||||
static void poll_pending_installs(void) {
|
||||
if (g_submitted_install_count <= 0)
|
||||
return;
|
||||
|
||||
invalidate_app_db_title_cache();
|
||||
|
||||
struct AppDbTitleList app_db_titles = {0};
|
||||
bool app_db_titles_ready = get_app_db_title_list_cached(&app_db_titles);
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
|
||||
if (!app_db_titles_ready)
|
||||
log_debug(" [DB] app.db title list unavailable while polling installs");
|
||||
|
||||
for (int i = 0; i < MAX_PENDING; i++) {
|
||||
pending_install_entry_t *entry = &g_pending_installs[i];
|
||||
if (entry->state != INSTALL_TRACK_SUBMITTED)
|
||||
continue;
|
||||
|
||||
if (app_db_titles_ready &&
|
||||
app_db_title_list_contains(&app_db_titles, entry->title_id)) {
|
||||
finalize_pending_install_success(entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (now_us != 0 && entry->requested_at_us != 0 &&
|
||||
now_us >= entry->requested_at_us &&
|
||||
now_us - entry->requested_at_us >= INSTALL_WAIT_TIMEOUT_US) {
|
||||
finalize_pending_install_timeout(entry);
|
||||
}
|
||||
}
|
||||
|
||||
schedule_pending_install_poll(now_us);
|
||||
if (g_submitted_install_count == 0)
|
||||
schedule_queued_install_submit(now_us);
|
||||
free_app_db_title_list(&app_db_titles);
|
||||
}
|
||||
|
||||
uint64_t sm_install_next_wake_us(uint64_t now_us) {
|
||||
if (runtime_sleep_mode_active())
|
||||
return 0;
|
||||
|
||||
if (!install_queue_active())
|
||||
return 0;
|
||||
|
||||
if (g_submitted_install_count <= 0)
|
||||
return g_queued_install_submit_due_us;
|
||||
|
||||
uint64_t next_wake_us = next_pending_install_timeout_us();
|
||||
|
||||
if (g_pending_install_poll_due_us != 0 &&
|
||||
(next_wake_us == 0 || g_pending_install_poll_due_us < next_wake_us)) {
|
||||
next_wake_us = g_pending_install_poll_due_us;
|
||||
}
|
||||
|
||||
if (g_queued_install_submit_due_us != 0 &&
|
||||
(next_wake_us == 0 || g_queued_install_submit_due_us < next_wake_us)) {
|
||||
next_wake_us = g_queued_install_submit_due_us;
|
||||
}
|
||||
|
||||
if (next_wake_us == 0)
|
||||
next_wake_us = now_us == 0 ? 1 : now_us + INSTALL_POLL_INTERVAL_US;
|
||||
|
||||
return next_wake_us;
|
||||
}
|
||||
|
||||
void sm_install_poll_pending(void) {
|
||||
if (runtime_sleep_mode_active())
|
||||
return;
|
||||
|
||||
poll_pending_installs();
|
||||
}
|
||||
|
||||
void sm_install_service_pending(void) {
|
||||
if (runtime_sleep_mode_active())
|
||||
return;
|
||||
|
||||
if (!install_queue_active())
|
||||
return;
|
||||
|
||||
sm_install_poll_pending();
|
||||
if (count_queued_installs() > 0 && !sm_install_submit_queued())
|
||||
sm_install_note_submit_failure();
|
||||
}
|
||||
|
||||
static void log_batch_submit_retry_limit(const pending_install_entry_t *entry) {
|
||||
uint8_t failed_attempts = get_failed_mount_attempts(entry->title_id);
|
||||
if (failed_attempts == MAX_FAILED_MOUNT_ATTEMPTS) {
|
||||
log_debug(" [RETRY] limit reached (%u/%u): %s (%s)",
|
||||
(unsigned)failed_attempts,
|
||||
(unsigned)MAX_FAILED_MOUNT_ATTEMPTS, entry->title_name,
|
||||
entry->title_id);
|
||||
}
|
||||
}
|
||||
|
||||
static int count_queued_installs(void) {
|
||||
int queued_count = g_tracked_install_count - g_submitted_install_count;
|
||||
return queued_count > 0 ? queued_count : 0;
|
||||
}
|
||||
|
||||
static void log_queued_install_batch(void) {
|
||||
int queued_count = count_queued_installs();
|
||||
if (queued_count <= 0)
|
||||
return;
|
||||
|
||||
log_debug(" [REG] Batch install request contains %d title(s):", queued_count);
|
||||
for (int i = 0; i < MAX_PENDING; i++) {
|
||||
const pending_install_entry_t *entry = &g_pending_installs[i];
|
||||
if (entry->state != INSTALL_TRACK_QUEUED)
|
||||
continue;
|
||||
log_debug(" - %s (%s)", entry->title_name, entry->title_id);
|
||||
}
|
||||
}
|
||||
|
||||
static void notify_queued_install_batch(void) {
|
||||
int queued_count = count_queued_installs();
|
||||
if (queued_count <= 0 || g_queued_install_batch_announced)
|
||||
return;
|
||||
|
||||
char message[3075];
|
||||
int shown_count = 0;
|
||||
snprintf(message, sizeof(message), "Batch install queued (%d):", queued_count);
|
||||
|
||||
for (int i = 0; i < MAX_PENDING; i++) {
|
||||
const pending_install_entry_t *entry = &g_pending_installs[i];
|
||||
if (entry->state != INSTALL_TRACK_QUEUED)
|
||||
continue;
|
||||
append_batch_install_line(message, sizeof(message), entry->title_name,
|
||||
entry->title_id, &shown_count);
|
||||
}
|
||||
|
||||
if (shown_count < queued_count) {
|
||||
size_t used = strlen(message);
|
||||
if (used < sizeof(message)) {
|
||||
(void)snprintf(message + used, sizeof(message) - used, "\n... and %d more",
|
||||
queued_count - shown_count);
|
||||
}
|
||||
}
|
||||
|
||||
notify_system_info("%s", message);
|
||||
g_queued_install_batch_announced = true;
|
||||
}
|
||||
|
||||
static uint8_t note_pending_install_failure(const pending_install_entry_t *entry) {
|
||||
if (!entry)
|
||||
return 0;
|
||||
|
||||
uint8_t failed_attempts = bump_failed_mount_attempts(entry->title_id);
|
||||
log_batch_submit_retry_limit(entry);
|
||||
return failed_attempts;
|
||||
}
|
||||
|
||||
static void drop_queued_install_entry(pending_install_entry_t *entry) {
|
||||
if (!entry || entry->state != INSTALL_TRACK_QUEUED)
|
||||
return;
|
||||
|
||||
log_debug(" [REG] Dropping queued install after retry limit: %s (%s)",
|
||||
entry->title_name, entry->title_id);
|
||||
clear_pending_install_entry(entry);
|
||||
}
|
||||
|
||||
bool sm_install_submit_queued(void) {
|
||||
if (runtime_sleep_mode_active())
|
||||
return true;
|
||||
|
||||
int queued_count = count_queued_installs();
|
||||
if (queued_count <= 0)
|
||||
return true;
|
||||
|
||||
if (g_submitted_install_count > 0)
|
||||
return true;
|
||||
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
if (g_queued_install_submit_due_us != 0 && now_us != 0 &&
|
||||
now_us < g_queued_install_submit_due_us) {
|
||||
return true;
|
||||
}
|
||||
|
||||
log_queued_install_batch();
|
||||
notify_queued_install_batch();
|
||||
|
||||
int res = sceAppInstUtilAppInstallAll();
|
||||
if (res != 0) {
|
||||
log_debug(" [REG] Batch install request failed: 0x%x", res);
|
||||
if (!g_queued_install_submit_failure_notified) {
|
||||
notify_system("Batch install failed for %d app(s).\ncode=0x%08X",
|
||||
queued_count, (uint32_t)res);
|
||||
g_queued_install_submit_failure_notified = true;
|
||||
}
|
||||
schedule_queued_install_submit_retry(now_us);
|
||||
return false;
|
||||
}
|
||||
|
||||
g_queued_install_submit_due_us = 0;
|
||||
g_queued_install_submit_failure_notified = false;
|
||||
for (int i = 0; i < MAX_PENDING; i++) {
|
||||
pending_install_entry_t *entry = &g_pending_installs[i];
|
||||
if (entry->state != INSTALL_TRACK_QUEUED)
|
||||
continue;
|
||||
entry->state = INSTALL_TRACK_SUBMITTED;
|
||||
entry->requested_at_us = now_us;
|
||||
g_submitted_install_count++;
|
||||
clear_failed_mount_attempts(entry->title_id);
|
||||
}
|
||||
|
||||
invalidate_app_db_title_cache();
|
||||
schedule_pending_install_poll(now_us);
|
||||
return true;
|
||||
}
|
||||
|
||||
void sm_install_note_submit_failure(void) {
|
||||
for (int i = 0; i < MAX_PENDING; i++) {
|
||||
pending_install_entry_t *entry = &g_pending_installs[i];
|
||||
if (entry->state != INSTALL_TRACK_QUEUED)
|
||||
continue;
|
||||
|
||||
if (note_pending_install_failure(entry) >= MAX_FAILED_MOUNT_ATTEMPTS)
|
||||
drop_queued_install_entry(entry);
|
||||
}
|
||||
}
|
||||
+920
@@ -0,0 +1,920 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_filesystem.h"
|
||||
#include "sm_kstuff.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_mdbg.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_time.h"
|
||||
#include "sm_types.h"
|
||||
|
||||
#include <stdatomic.h>
|
||||
|
||||
#define KSTUFF_SYSENTVEC_TOGGLE_OFFSET 14
|
||||
#define KSTUFF_SYSENTVEC_ENABLED 0xdeb7u
|
||||
#define KSTUFF_SYSENTVEC_DISABLED 0xffffu
|
||||
typedef struct {
|
||||
bool active;
|
||||
bool image_backed;
|
||||
bool autopause_delay_valid;
|
||||
bool pause_applied;
|
||||
bool focus_override_active;
|
||||
pid_t pid;
|
||||
uint32_t app_id;
|
||||
uint32_t autopause_delay_seconds;
|
||||
uint64_t launch_time_us;
|
||||
uint64_t pause_deadline_us;
|
||||
char title_id[MAX_TITLE_ID];
|
||||
} kstuff_game_entry_t;
|
||||
|
||||
typedef struct {
|
||||
intptr_t sysentvec_ps5;
|
||||
intptr_t sysentvec_ps4;
|
||||
bool probe_available;
|
||||
bool supported;
|
||||
bool restore_on_empty;
|
||||
bool sleep_pause_restore;
|
||||
bool current_app_focus_valid;
|
||||
uint32_t current_app_focus_id;
|
||||
uint64_t restore_retry_us;
|
||||
kstuff_game_entry_t game;
|
||||
} kstuff_state_t;
|
||||
|
||||
static kstuff_state_t g_kstuff;
|
||||
static _Atomic uint32_t g_pending_app_focus_id;
|
||||
static _Atomic bool g_pending_app_focus_valid;
|
||||
static _Atomic bool g_pending_config_reload;
|
||||
static uint32_t get_effective_pause_delay_seconds(const kstuff_game_entry_t *entry);
|
||||
|
||||
static bool refresh_kstuff_support_state(void);
|
||||
static uint32_t get_pause_delay_seconds_for_title(const char *title_id,
|
||||
bool *image_backed_out,
|
||||
uint32_t *autopause_delay_out,
|
||||
bool *autopause_delay_valid_out);
|
||||
static void restore_kstuff_if_needed(const char *reason);
|
||||
|
||||
static char *trim_ascii_inplace(char *s) {
|
||||
while (*s == ' ' || *s == '\t' || *s == '\r' || *s == '\n')
|
||||
s++;
|
||||
|
||||
size_t len = strlen(s);
|
||||
while (len > 0) {
|
||||
char ch = s[len - 1u];
|
||||
if (ch != ' ' && ch != '\t' && ch != '\r' && ch != '\n')
|
||||
break;
|
||||
s[len - 1u] = '\0';
|
||||
len--;
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
static char *strip_inline_comment(char *s) {
|
||||
char *comment = strpbrk(s, "#;");
|
||||
if (comment)
|
||||
*comment = '\0';
|
||||
return trim_ascii_inplace(s);
|
||||
}
|
||||
|
||||
static bool parse_pause_delay_seconds(const char *value,
|
||||
uint32_t *delay_seconds_out) {
|
||||
if (!value || !delay_seconds_out)
|
||||
return false;
|
||||
|
||||
errno = 0;
|
||||
char *end = NULL;
|
||||
unsigned long parsed = strtoul(value, &end, 10);
|
||||
if (errno != 0 || end == value || *end != '\0' ||
|
||||
parsed > MAX_KSTUFF_PAUSE_DELAY_SECONDS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*delay_seconds_out = (uint32_t)parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool get_autopause_delay_for_source(const char *source_path,
|
||||
bool image_backed,
|
||||
uint32_t *delay_seconds_out) {
|
||||
if (!source_path || source_path[0] == '\0' || !delay_seconds_out)
|
||||
return false;
|
||||
|
||||
char autopause_path[MAX_PATH];
|
||||
int written = snprintf(autopause_path, sizeof(autopause_path), "%s/%s",
|
||||
source_path, "autopause.txt");
|
||||
if (written <= 0 || (size_t)written >= sizeof(autopause_path))
|
||||
return false;
|
||||
|
||||
FILE *f = fopen(autopause_path, "r");
|
||||
if (!f)
|
||||
return false;
|
||||
|
||||
char line[128];
|
||||
bool have_scalar = false;
|
||||
bool have_direct = false;
|
||||
bool have_image = false;
|
||||
uint32_t scalar_delay = 0;
|
||||
uint32_t direct_delay = 0;
|
||||
uint32_t image_delay = 0;
|
||||
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
char *value = strip_inline_comment(line);
|
||||
if (value[0] == '\0')
|
||||
continue;
|
||||
|
||||
char *eq = strchr(value, '=');
|
||||
if (!eq) {
|
||||
if (!have_scalar && parse_pause_delay_seconds(value, &scalar_delay))
|
||||
have_scalar = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
*eq = '\0';
|
||||
char *key = trim_ascii_inplace(value);
|
||||
char *arg = trim_ascii_inplace(eq + 1);
|
||||
uint32_t parsed_delay = 0;
|
||||
if (!parse_pause_delay_seconds(arg, &parsed_delay))
|
||||
continue;
|
||||
|
||||
if (strcasecmp(key, "direct") == 0) {
|
||||
direct_delay = parsed_delay;
|
||||
have_direct = true;
|
||||
} else if (strcasecmp(key, "image") == 0) {
|
||||
image_delay = parsed_delay;
|
||||
have_image = true;
|
||||
}
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
|
||||
if (image_backed && have_image) {
|
||||
*delay_seconds_out = image_delay;
|
||||
return true;
|
||||
}
|
||||
if (!image_backed && have_direct) {
|
||||
*delay_seconds_out = direct_delay;
|
||||
return true;
|
||||
}
|
||||
if (have_scalar) {
|
||||
uint64_t delay_seconds = scalar_delay;
|
||||
if (image_backed)
|
||||
delay_seconds *= 2u;
|
||||
if (delay_seconds > MAX_KSTUFF_PAUSE_DELAY_SECONDS)
|
||||
delay_seconds = MAX_KSTUFF_PAUSE_DELAY_SECONDS;
|
||||
*delay_seconds_out = (uint32_t)delay_seconds;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool resolve_title_pause_source(const char *title_id,
|
||||
char source_path[MAX_PATH],
|
||||
bool *image_backed_out) {
|
||||
source_path[0] = '\0';
|
||||
*image_backed_out = false;
|
||||
|
||||
if (!title_id)
|
||||
return false;
|
||||
|
||||
if (!read_mount_link(title_id, source_path, MAX_PATH))
|
||||
return false;
|
||||
|
||||
*image_backed_out = is_under_image_mount_base(source_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint32_t get_configured_pause_delay_seconds(const char *title_id,
|
||||
bool image_backed,
|
||||
bool *override_applied_out) {
|
||||
const runtime_config_t *cfg = runtime_config();
|
||||
uint32_t delay_seconds =
|
||||
image_backed ? cfg->kstuff_pause_delay_image_seconds
|
||||
: cfg->kstuff_pause_delay_direct_seconds;
|
||||
uint32_t override_delay_seconds = 0;
|
||||
if (get_kstuff_pause_delay_override_for_title(title_id,
|
||||
&override_delay_seconds)) {
|
||||
*override_applied_out = true;
|
||||
return override_delay_seconds;
|
||||
}
|
||||
|
||||
*override_applied_out = false;
|
||||
return delay_seconds;
|
||||
}
|
||||
|
||||
static bool resolve_kstuff_sysentvec_addrs(intptr_t *ps5_out, intptr_t *ps4_out) {
|
||||
if (!ps5_out || !ps4_out)
|
||||
return false;
|
||||
|
||||
*ps5_out = 0;
|
||||
*ps4_out = 0;
|
||||
|
||||
switch (kernel_get_fw_version() & 0xffff0000u) {
|
||||
case 0x1000000:
|
||||
case 0x1010000:
|
||||
case 0x1020000:
|
||||
case 0x1050000:
|
||||
case 0x1100000:
|
||||
case 0x1110000:
|
||||
case 0x1120000:
|
||||
case 0x1130000:
|
||||
case 0x1140000:
|
||||
case 0x2000000:
|
||||
case 0x2200000:
|
||||
case 0x2250000:
|
||||
case 0x2260000:
|
||||
case 0x2300000:
|
||||
case 0x2500000:
|
||||
case 0x2700000:
|
||||
// Older jailbreak chains usually rely on Byepervisor instead of kstuff.
|
||||
return false;
|
||||
|
||||
case 0x3000000:
|
||||
case 0x3100000:
|
||||
case 0x3200000:
|
||||
case 0x3210000:
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xca0cd8;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xca0e50;
|
||||
return true;
|
||||
|
||||
case 0x4000000:
|
||||
case 0x4020000:
|
||||
case 0x4030000:
|
||||
case 0x4500000:
|
||||
case 0x4510000:
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xd11bb8;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xd11d30;
|
||||
return true;
|
||||
|
||||
case 0x5000000:
|
||||
case 0x5020000:
|
||||
case 0x5100000:
|
||||
case 0x5500000:
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xe00be8;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xe00d60;
|
||||
return true;
|
||||
|
||||
case 0x6000000:
|
||||
case 0x6020000:
|
||||
case 0x6500000:
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xe210a8;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xe21220;
|
||||
return true;
|
||||
|
||||
case 0x7000000:
|
||||
case 0x7010000:
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xe21ab8;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xe21c30;
|
||||
return true;
|
||||
|
||||
case 0x7200000:
|
||||
case 0x7400000:
|
||||
case 0x7600000:
|
||||
case 0x7610000:
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xe21b78;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xe21cf0;
|
||||
return true;
|
||||
|
||||
case 0x8000000:
|
||||
case 0x8200000:
|
||||
case 0x8400000:
|
||||
case 0x8600000:
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xe21ca8;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xe21e20;
|
||||
return true;
|
||||
|
||||
case 0x9000000:
|
||||
case 0x9050000:
|
||||
case 0x9200000:
|
||||
case 0x9400000:
|
||||
case 0x9600000:
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xdba648;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xdba7c0;
|
||||
return true;
|
||||
|
||||
case 0x10000000:
|
||||
case 0x10010000:
|
||||
case 0x10200000:
|
||||
case 0x10400000:
|
||||
case 0x10600000:
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xdba6d8;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xdba850;
|
||||
return true;
|
||||
|
||||
case 0x11000000:
|
||||
case 0x11200000:
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xdcbc78;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xdcbdf0;
|
||||
return true;
|
||||
|
||||
case 0x11400000:
|
||||
case 0x11600000:
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xdcbc98;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xdcbe10;
|
||||
return true;
|
||||
|
||||
case 0x12000000:
|
||||
case 0x12020000:
|
||||
case 0x12200000:
|
||||
case 0x12400000:
|
||||
case 0x12600000:
|
||||
case 0x12700000:
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xdcc978;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xdccaf0;
|
||||
return true;
|
||||
|
||||
case 0x13000000:
|
||||
case 0x13200000:
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xddd8a8;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xddda20;
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool kstuff_sysentvec_is_enabled(intptr_t sysentvec_addr) {
|
||||
return kernel_getshort(sysentvec_addr + KSTUFF_SYSENTVEC_TOGGLE_OFFSET) !=
|
||||
KSTUFF_SYSENTVEC_DISABLED;
|
||||
}
|
||||
|
||||
static uint16_t read_kstuff_sysentvec_toggle(intptr_t sysentvec_addr) {
|
||||
return (uint16_t)kernel_getshort(sysentvec_addr +
|
||||
KSTUFF_SYSENTVEC_TOGGLE_OFFSET);
|
||||
}
|
||||
|
||||
static bool kstuff_sysentvec_toggle_is_known(uint16_t value) {
|
||||
return value == KSTUFF_SYSENTVEC_ENABLED ||
|
||||
value == KSTUFF_SYSENTVEC_DISABLED;
|
||||
}
|
||||
|
||||
static void set_kstuff_sysentvec_enabled(intptr_t sysentvec_addr, bool enabled) {
|
||||
(void)kernel_setshort(sysentvec_addr + KSTUFF_SYSENTVEC_TOGGLE_OFFSET,
|
||||
enabled ? KSTUFF_SYSENTVEC_ENABLED
|
||||
: KSTUFF_SYSENTVEC_DISABLED);
|
||||
}
|
||||
|
||||
static bool read_kstuff_enabled_state(bool *ps5_out, bool *ps4_out) {
|
||||
if (!refresh_kstuff_support_state())
|
||||
return false;
|
||||
|
||||
bool ps5_enabled = kstuff_sysentvec_is_enabled(g_kstuff.sysentvec_ps5);
|
||||
bool ps4_enabled = kstuff_sysentvec_is_enabled(g_kstuff.sysentvec_ps4);
|
||||
if (ps5_out)
|
||||
*ps5_out = ps5_enabled;
|
||||
if (ps4_out)
|
||||
*ps4_out = ps4_enabled;
|
||||
return ps5_enabled && ps4_enabled;
|
||||
}
|
||||
|
||||
static bool refresh_kstuff_support_state(void) {
|
||||
if (!g_kstuff.probe_available) {
|
||||
g_kstuff.supported = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t ps5_toggle = read_kstuff_sysentvec_toggle(g_kstuff.sysentvec_ps5);
|
||||
uint16_t ps4_toggle = read_kstuff_sysentvec_toggle(g_kstuff.sysentvec_ps4);
|
||||
g_kstuff.supported = kstuff_sysentvec_toggle_is_known(ps5_toggle) &&
|
||||
kstuff_sysentvec_toggle_is_known(ps4_toggle);
|
||||
return g_kstuff.supported;
|
||||
}
|
||||
|
||||
static bool kstuff_state_matches(bool enabled, bool ps5_enabled,
|
||||
bool ps4_enabled) {
|
||||
return ps5_enabled == enabled && ps4_enabled == enabled;
|
||||
}
|
||||
|
||||
static bool apply_kstuff_enabled_state(bool enabled, bool notify_user,
|
||||
bool *fully_applied_out);
|
||||
|
||||
static void mark_restore_needed(void) {
|
||||
g_kstuff.restore_on_empty = true;
|
||||
g_kstuff.restore_retry_us = 0;
|
||||
}
|
||||
|
||||
static bool tracked_game_requires_restore(void) {
|
||||
return g_kstuff.restore_on_empty;
|
||||
}
|
||||
|
||||
static void finish_tracked_game_clear(const char *reason) {
|
||||
if (!g_kstuff.restore_on_empty)
|
||||
return;
|
||||
|
||||
mark_restore_needed();
|
||||
restore_kstuff_if_needed(reason);
|
||||
}
|
||||
|
||||
static void apply_kstuff_config_reload(void) {
|
||||
if (!runtime_config()->kstuff_crash_detection_enabled)
|
||||
sm_mdbg_game_shutdown();
|
||||
|
||||
if (!runtime_config()->kstuff_game_auto_toggle) {
|
||||
sm_kstuff_game_shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!g_kstuff.game.active)
|
||||
return;
|
||||
|
||||
if (is_kstuff_pause_disabled_for_title(g_kstuff.game.title_id)) {
|
||||
bool restore_needed = tracked_game_requires_restore();
|
||||
log_debug(" [KSTUFF] cleared tracked game by config reload: %s",
|
||||
g_kstuff.game.title_id);
|
||||
sm_mdbg_game_shutdown();
|
||||
memset(&g_kstuff.game, 0, sizeof(g_kstuff.game));
|
||||
if (restore_needed)
|
||||
finish_tracked_game_clear("config reload");
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_kstuff.game.pause_applied)
|
||||
return;
|
||||
|
||||
bool image_backed = g_kstuff.game.image_backed;
|
||||
bool autopause_delay_valid = false;
|
||||
uint32_t autopause_delay_seconds = 0;
|
||||
uint32_t delay_seconds = get_pause_delay_seconds_for_title(
|
||||
g_kstuff.game.title_id, &image_backed, &autopause_delay_seconds,
|
||||
&autopause_delay_valid);
|
||||
g_kstuff.game.image_backed = image_backed;
|
||||
g_kstuff.game.autopause_delay_valid = autopause_delay_valid;
|
||||
g_kstuff.game.autopause_delay_seconds = autopause_delay_seconds;
|
||||
g_kstuff.game.pause_deadline_us =
|
||||
g_kstuff.game.launch_time_us == 0
|
||||
? 0
|
||||
: g_kstuff.game.launch_time_us + (uint64_t)delay_seconds * 1000000ull;
|
||||
log_debug(" [KSTUFF] updated tracked game config: %s source=%s pause_delay=%us",
|
||||
g_kstuff.game.title_id,
|
||||
g_kstuff.game.image_backed ? "image" : "direct",
|
||||
delay_seconds);
|
||||
}
|
||||
|
||||
static uint32_t get_pause_delay_seconds_for_title(const char *title_id,
|
||||
bool *image_backed_out,
|
||||
uint32_t *autopause_delay_out,
|
||||
bool *autopause_delay_valid_out) {
|
||||
char source_path[MAX_PATH];
|
||||
bool image_backed = false;
|
||||
(void)resolve_title_pause_source(title_id, source_path, &image_backed);
|
||||
|
||||
*image_backed_out = image_backed;
|
||||
*autopause_delay_out = 0;
|
||||
*autopause_delay_valid_out = false;
|
||||
|
||||
uint32_t autotuned_delay_seconds = 0;
|
||||
if (get_kstuff_autotune_pause_delay_for_title(title_id,
|
||||
&autotuned_delay_seconds)) {
|
||||
return autotuned_delay_seconds;
|
||||
}
|
||||
|
||||
bool override_applied = false;
|
||||
uint32_t delay_seconds =
|
||||
get_configured_pause_delay_seconds(title_id, image_backed,
|
||||
&override_applied);
|
||||
uint32_t autopause_delay_seconds = 0;
|
||||
if (get_autopause_delay_for_source(source_path, image_backed,
|
||||
&autopause_delay_seconds)) {
|
||||
*autopause_delay_out = autopause_delay_seconds;
|
||||
*autopause_delay_valid_out = true;
|
||||
if (!override_applied)
|
||||
return autopause_delay_seconds;
|
||||
}
|
||||
|
||||
return delay_seconds;
|
||||
}
|
||||
|
||||
static void restore_kstuff_if_needed(const char *reason) {
|
||||
if (g_kstuff.game.active && g_kstuff.game.pause_applied)
|
||||
return;
|
||||
|
||||
if (!g_kstuff.restore_on_empty)
|
||||
return;
|
||||
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
if (now_us != 0 && g_kstuff.restore_retry_us != 0 &&
|
||||
now_us < g_kstuff.restore_retry_us) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_kstuff.game.active && !g_kstuff.game.pause_applied) {
|
||||
if (g_kstuff.game.pause_deadline_us == 0 ||
|
||||
(now_us != 0 && now_us >= g_kstuff.game.pause_deadline_us)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sm_kstuff_is_supported()) {
|
||||
g_kstuff.restore_retry_us =
|
||||
now_us == 0 ? 1 : now_us + GAME_LIFECYCLE_POLL_INTERVAL_US;
|
||||
return;
|
||||
}
|
||||
|
||||
bool enabled = sm_kstuff_is_enabled();
|
||||
if (!enabled)
|
||||
enabled = sm_kstuff_set_enabled(true, true);
|
||||
if (enabled) {
|
||||
log_debug(" [KSTUFF] auto-resumed (%s)%s",
|
||||
reason ? reason : "no reason",
|
||||
g_kstuff.game.active ? ", pending delay for another tracked game"
|
||||
: "");
|
||||
} else {
|
||||
log_debug(" [KSTUFF] failed to auto-resume (%s)",
|
||||
reason ? reason : "no reason");
|
||||
g_kstuff.restore_retry_us =
|
||||
now_us == 0 ? 0 : now_us + KSTUFF_RESTORE_RETRY_US;
|
||||
return;
|
||||
}
|
||||
|
||||
g_kstuff.restore_on_empty = false;
|
||||
g_kstuff.restore_retry_us = 0;
|
||||
}
|
||||
|
||||
static void maybe_apply_kstuff_pause_for_slot(kstuff_game_entry_t *entry) {
|
||||
if (!entry->active || entry->pause_applied)
|
||||
return;
|
||||
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
if (now_us != 0 && now_us < entry->pause_deadline_us)
|
||||
return;
|
||||
|
||||
if (g_kstuff.current_app_focus_valid && entry->app_id != 0 &&
|
||||
g_kstuff.current_app_focus_id != entry->app_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sm_kstuff_is_supported()) {
|
||||
log_debug(" [KSTUFF] skipping auto-pause for %s pid=%ld: kstuff not "
|
||||
"available",
|
||||
entry->title_id, (long)entry->pid);
|
||||
sm_mdbg_game_shutdown();
|
||||
memset(entry, 0, sizeof(*entry));
|
||||
return;
|
||||
}
|
||||
|
||||
bool was_enabled = sm_kstuff_is_enabled();
|
||||
bool fully_disabled = false;
|
||||
(void)apply_kstuff_enabled_state(false, was_enabled, &fully_disabled);
|
||||
if (!fully_disabled) {
|
||||
log_debug(" [KSTUFF] failed to auto-pause for %s pid=%ld",
|
||||
entry->title_id, (long)entry->pid);
|
||||
return;
|
||||
}
|
||||
|
||||
if (was_enabled) {
|
||||
mark_restore_needed();
|
||||
sm_mdbg_game_on_kstuff_pause(entry->pid, now_us,
|
||||
get_effective_pause_delay_seconds(entry));
|
||||
log_debug(" [KSTUFF] auto-paused for %s pid=%ld (%s launch)",
|
||||
entry->title_id, (long)entry->pid,
|
||||
entry->image_backed ? "image" : "direct");
|
||||
} else {
|
||||
log_debug(" [KSTUFF] pause deadline reached for %s pid=%ld but kstuff "
|
||||
"was already disabled", entry->title_id, (long)entry->pid);
|
||||
}
|
||||
|
||||
entry->pause_applied = true;
|
||||
}
|
||||
|
||||
static void handle_kstuff_app_focus_change(uint32_t focused_app_id) {
|
||||
g_kstuff.current_app_focus_valid = true;
|
||||
g_kstuff.current_app_focus_id = focused_app_id;
|
||||
|
||||
if (!g_kstuff.game.active || g_kstuff.game.app_id == 0)
|
||||
return;
|
||||
|
||||
if (focused_app_id != g_kstuff.game.app_id) {
|
||||
if (!g_kstuff.game.pause_applied)
|
||||
return;
|
||||
|
||||
if (!g_kstuff.restore_on_empty)
|
||||
return;
|
||||
|
||||
if (g_kstuff.game.focus_override_active)
|
||||
return;
|
||||
|
||||
bool enabled = sm_kstuff_is_enabled();
|
||||
if (!enabled)
|
||||
enabled = sm_kstuff_set_enabled(true, false);
|
||||
if (!enabled) {
|
||||
log_debug(" [KSTUFF] failed to enable on app focus leave "
|
||||
"(tracked=0x%08X current=0x%08X)",
|
||||
g_kstuff.game.app_id, focused_app_id);
|
||||
return;
|
||||
}
|
||||
|
||||
g_kstuff.game.focus_override_active = true;
|
||||
log_debug(" [KSTUFF] temporarily enabled on app focus leave "
|
||||
"(tracked=0x%08X current=0x%08X)",
|
||||
g_kstuff.game.app_id, focused_app_id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!g_kstuff.game.focus_override_active)
|
||||
return;
|
||||
|
||||
bool fully_disabled = false;
|
||||
(void)apply_kstuff_enabled_state(false, false, &fully_disabled);
|
||||
if (!fully_disabled) {
|
||||
log_debug(" [KSTUFF] failed to restore disabled state on app focus return "
|
||||
"(app_id=0x%08X)",
|
||||
focused_app_id);
|
||||
return;
|
||||
}
|
||||
|
||||
g_kstuff.game.focus_override_active = false;
|
||||
log_debug(" [KSTUFF] restored disabled state on app focus return "
|
||||
"(app_id=0x%08X)",
|
||||
focused_app_id);
|
||||
}
|
||||
|
||||
uint64_t sm_kstuff_game_next_wake_us(uint64_t now_us) {
|
||||
if (!sm_kstuff_game_feature_enabled())
|
||||
return 0;
|
||||
|
||||
uint64_t next_wake_us = 0;
|
||||
if (g_kstuff.game.active && !g_kstuff.game.pause_applied) {
|
||||
if (g_kstuff.current_app_focus_valid && g_kstuff.game.app_id != 0 &&
|
||||
g_kstuff.current_app_focus_id != g_kstuff.game.app_id) {
|
||||
next_wake_us = 0;
|
||||
} else if (g_kstuff.game.pause_deadline_us != 0) {
|
||||
next_wake_us = g_kstuff.game.pause_deadline_us;
|
||||
} else if (now_us == 0) {
|
||||
next_wake_us = 1;
|
||||
} else {
|
||||
next_wake_us = now_us + GAME_LIFECYCLE_POLL_INTERVAL_US;
|
||||
}
|
||||
}
|
||||
|
||||
if (g_kstuff.restore_on_empty) {
|
||||
uint64_t restore_wake_us = g_kstuff.restore_retry_us;
|
||||
if (restore_wake_us == 0) {
|
||||
if (g_kstuff.game.active && !g_kstuff.game.pause_applied) {
|
||||
if (g_kstuff.game.pause_deadline_us != 0) {
|
||||
restore_wake_us = g_kstuff.game.pause_deadline_us;
|
||||
} else if (now_us == 0) {
|
||||
restore_wake_us = 1;
|
||||
} else {
|
||||
restore_wake_us = now_us + GAME_LIFECYCLE_POLL_INTERVAL_US;
|
||||
}
|
||||
} else {
|
||||
restore_wake_us = now_us == 0 ? 1 : now_us;
|
||||
}
|
||||
}
|
||||
|
||||
if (next_wake_us == 0 || restore_wake_us < next_wake_us)
|
||||
next_wake_us = restore_wake_us;
|
||||
}
|
||||
|
||||
return next_wake_us;
|
||||
}
|
||||
|
||||
bool sm_kstuff_is_supported(void) {
|
||||
return refresh_kstuff_support_state();
|
||||
}
|
||||
|
||||
bool sm_kstuff_is_enabled(void) {
|
||||
return read_kstuff_enabled_state(NULL, NULL);
|
||||
}
|
||||
|
||||
static bool apply_kstuff_enabled_state(bool enabled, bool notify_user,
|
||||
bool *fully_applied_out) {
|
||||
bool ps5_enabled = false;
|
||||
bool ps4_enabled = false;
|
||||
bool is_enabled = read_kstuff_enabled_state(&ps5_enabled, &ps4_enabled);
|
||||
if (!g_kstuff.supported)
|
||||
return false;
|
||||
bool fully_applied = kstuff_state_matches(enabled, ps5_enabled, ps4_enabled);
|
||||
if (fully_applied) {
|
||||
if (fully_applied_out)
|
||||
*fully_applied_out = true;
|
||||
return is_enabled;
|
||||
}
|
||||
|
||||
if (ps5_enabled != enabled)
|
||||
set_kstuff_sysentvec_enabled(g_kstuff.sysentvec_ps5, enabled);
|
||||
if (ps4_enabled != enabled)
|
||||
set_kstuff_sysentvec_enabled(g_kstuff.sysentvec_ps4, enabled);
|
||||
|
||||
is_enabled = read_kstuff_enabled_state(&ps5_enabled, &ps4_enabled);
|
||||
fully_applied = kstuff_state_matches(enabled, ps5_enabled, ps4_enabled);
|
||||
if (fully_applied_out)
|
||||
*fully_applied_out = fully_applied;
|
||||
|
||||
if (fully_applied) {
|
||||
log_debug(" [KSTUFF] %s both sysentvecs", enabled ? "enabled" : "disabled");
|
||||
if (notify_user) {
|
||||
notify_system_info(enabled ? "KStuff active again"
|
||||
: "KStuff paused while the game is running");
|
||||
}
|
||||
} else {
|
||||
log_debug(" [KSTUFF] %s request incomplete (ps5=%s ps4=%s)",
|
||||
enabled ? "enable" : "disable", ps5_enabled ? "on" : "off",
|
||||
ps4_enabled ? "on" : "off");
|
||||
}
|
||||
|
||||
return is_enabled;
|
||||
}
|
||||
|
||||
bool sm_kstuff_set_enabled(bool enabled, bool notify_user) {
|
||||
return apply_kstuff_enabled_state(enabled, notify_user, NULL);
|
||||
}
|
||||
|
||||
bool sm_kstuff_game_feature_enabled(void) {
|
||||
return runtime_config()->kstuff_game_auto_toggle && g_kstuff.probe_available;
|
||||
}
|
||||
|
||||
void sm_kstuff_game_on_exec(pid_t pid, const char *title_id, uint32_t app_id,
|
||||
uint64_t exec_time_us) {
|
||||
if (!sm_kstuff_game_feature_enabled())
|
||||
return;
|
||||
|
||||
if (g_kstuff.game.active) {
|
||||
if (g_kstuff.game.pid == pid) {
|
||||
log_debug(" [KSTUFF] already tracking pid=%ld for %s", (long)pid,
|
||||
title_id);
|
||||
return;
|
||||
}
|
||||
|
||||
bool restore_needed = tracked_game_requires_restore();
|
||||
log_debug(" [KSTUFF] handoff tracked game pid=%ld (%s) -> pid=%ld (%s)",
|
||||
(long)g_kstuff.game.pid, g_kstuff.game.title_id, (long)pid,
|
||||
title_id);
|
||||
sm_mdbg_game_shutdown();
|
||||
memset(&g_kstuff.game, 0, sizeof(g_kstuff.game));
|
||||
if (restore_needed)
|
||||
finish_tracked_game_clear("tracked game handoff");
|
||||
}
|
||||
|
||||
if (is_kstuff_pause_disabled_for_title(title_id)) {
|
||||
log_debug(" [KSTUFF] auto-pause disabled by config for %s pid=%ld "
|
||||
"app_id=0x%08X", title_id, (long)pid, app_id);
|
||||
return;
|
||||
}
|
||||
|
||||
bool image_backed = false;
|
||||
bool autopause_delay_valid = false;
|
||||
uint32_t autopause_delay_seconds = 0;
|
||||
uint32_t delay_seconds = get_pause_delay_seconds_for_title(
|
||||
title_id, &image_backed, &autopause_delay_seconds,
|
||||
&autopause_delay_valid);
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
uint64_t base_time_us = exec_time_us != 0 ? exec_time_us : now_us;
|
||||
|
||||
g_kstuff.game.active = true;
|
||||
g_kstuff.game.image_backed = image_backed;
|
||||
g_kstuff.game.autopause_delay_valid = autopause_delay_valid;
|
||||
g_kstuff.game.pause_applied = false;
|
||||
g_kstuff.game.focus_override_active = false;
|
||||
g_kstuff.game.pid = pid;
|
||||
g_kstuff.game.app_id = app_id;
|
||||
g_kstuff.game.autopause_delay_seconds = autopause_delay_seconds;
|
||||
g_kstuff.game.launch_time_us = base_time_us;
|
||||
g_kstuff.game.pause_deadline_us =
|
||||
base_time_us == 0 ? 0 : base_time_us + (uint64_t)delay_seconds * 1000000ull;
|
||||
(void)strlcpy(g_kstuff.game.title_id, title_id, sizeof(g_kstuff.game.title_id));
|
||||
sm_mdbg_game_on_exec(pid, title_id, app_id);
|
||||
|
||||
log_debug(" [KSTUFF] tracking game launch: %s pid=%ld app_id=0x%08X source=%s "
|
||||
"pause_delay=%us", title_id, (long)pid, app_id,
|
||||
image_backed ? "image" : "direct", delay_seconds);
|
||||
}
|
||||
|
||||
void sm_kstuff_note_app_focus(uint32_t app_id) {
|
||||
atomic_store(&g_pending_app_focus_id, app_id);
|
||||
atomic_store(&g_pending_app_focus_valid, true);
|
||||
}
|
||||
|
||||
void sm_kstuff_game_on_exit(pid_t pid) {
|
||||
if (!g_kstuff.game.active || g_kstuff.game.pid != pid)
|
||||
return;
|
||||
|
||||
bool restore_needed = tracked_game_requires_restore();
|
||||
sm_mdbg_game_on_exit(pid);
|
||||
log_debug(" [KSTUFF] game stopped: %s pid=%ld",
|
||||
g_kstuff.game.title_id, (long)pid);
|
||||
memset(&g_kstuff.game, 0, sizeof(g_kstuff.game));
|
||||
if (restore_needed)
|
||||
finish_tracked_game_clear("tracked game exit");
|
||||
}
|
||||
|
||||
void sm_kstuff_game_poll(void) {
|
||||
if (atomic_exchange(&g_pending_config_reload, false))
|
||||
apply_kstuff_config_reload();
|
||||
|
||||
if (!sm_kstuff_game_feature_enabled())
|
||||
return;
|
||||
|
||||
if (atomic_exchange(&g_pending_app_focus_valid, false)) {
|
||||
handle_kstuff_app_focus_change(atomic_load(&g_pending_app_focus_id));
|
||||
}
|
||||
|
||||
restore_kstuff_if_needed("pending auto-resume");
|
||||
maybe_apply_kstuff_pause_for_slot(&g_kstuff.game);
|
||||
}
|
||||
|
||||
void sm_kstuff_game_shutdown(void) {
|
||||
bool restore_needed = tracked_game_requires_restore();
|
||||
if (restore_needed && !g_kstuff.sleep_pause_restore &&
|
||||
runtime_sleep_mode_active() && should_stop_requested()) {
|
||||
restore_needed = false;
|
||||
g_kstuff.restore_on_empty = false;
|
||||
g_kstuff.restore_retry_us = 0;
|
||||
} else if (g_kstuff.sleep_pause_restore) {
|
||||
restore_needed = true;
|
||||
g_kstuff.sleep_pause_restore = false;
|
||||
}
|
||||
sm_mdbg_game_shutdown();
|
||||
memset(&g_kstuff.game, 0, sizeof(g_kstuff.game));
|
||||
if (restore_needed) {
|
||||
mark_restore_needed();
|
||||
restore_kstuff_if_needed("watcher shutdown");
|
||||
}
|
||||
}
|
||||
|
||||
void sm_kstuff_sleep_enter(void) {
|
||||
bool was_enabled = false;
|
||||
bool fully_disabled = false;
|
||||
|
||||
sm_mdbg_game_shutdown();
|
||||
memset(&g_kstuff.game, 0, sizeof(g_kstuff.game));
|
||||
g_kstuff.sleep_pause_restore = false;
|
||||
|
||||
if (!sm_kstuff_is_supported())
|
||||
return;
|
||||
|
||||
was_enabled = sm_kstuff_is_enabled();
|
||||
(void)apply_kstuff_enabled_state(false, false, &fully_disabled);
|
||||
if (!fully_disabled) {
|
||||
log_debug(" [KSTUFF] failed to pause for system sleep");
|
||||
return;
|
||||
}
|
||||
|
||||
g_kstuff.sleep_pause_restore = was_enabled;
|
||||
if (was_enabled)
|
||||
log_debug(" [KSTUFF] paused for system sleep");
|
||||
}
|
||||
|
||||
void sm_kstuff_sleep_leave(void) {
|
||||
if (!g_kstuff.sleep_pause_restore)
|
||||
return;
|
||||
|
||||
g_kstuff.sleep_pause_restore = false;
|
||||
mark_restore_needed();
|
||||
restore_kstuff_if_needed("system sleep");
|
||||
}
|
||||
|
||||
void sm_kstuff_on_config_reload(void) {
|
||||
atomic_store(&g_pending_config_reload, true);
|
||||
}
|
||||
|
||||
void sm_kstuff_init(void) {
|
||||
memset(&g_kstuff, 0, sizeof(g_kstuff));
|
||||
atomic_store(&g_pending_app_focus_id, 0);
|
||||
atomic_store(&g_pending_app_focus_valid, false);
|
||||
atomic_store(&g_pending_config_reload, false);
|
||||
|
||||
if (!resolve_kstuff_sysentvec_addrs(&g_kstuff.sysentvec_ps5,
|
||||
&g_kstuff.sysentvec_ps4)) {
|
||||
log_debug(" [KSTUFF] runtime control unavailable on firmware 0x%08X",
|
||||
kernel_get_fw_version());
|
||||
return;
|
||||
}
|
||||
g_kstuff.probe_available = true;
|
||||
|
||||
if (!refresh_kstuff_support_state()) {
|
||||
log_debug(" [KSTUFF] runtime control probe ready; kstuff not present at "
|
||||
"startup");
|
||||
return;
|
||||
}
|
||||
|
||||
log_debug(" [KSTUFF] runtime control ready (image_delay=%u "
|
||||
"direct_delay=%u)",
|
||||
runtime_config()->kstuff_pause_delay_image_seconds,
|
||||
runtime_config()->kstuff_pause_delay_direct_seconds);
|
||||
}
|
||||
|
||||
void sm_kstuff_shutdown(void) {
|
||||
sm_kstuff_game_shutdown();
|
||||
atomic_store(&g_pending_app_focus_id, 0);
|
||||
atomic_store(&g_pending_app_focus_valid, false);
|
||||
atomic_store(&g_pending_config_reload, false);
|
||||
memset(&g_kstuff, 0, sizeof(g_kstuff));
|
||||
}
|
||||
|
||||
static uint32_t get_effective_pause_delay_seconds(const kstuff_game_entry_t *entry) {
|
||||
if (!entry || entry->launch_time_us == 0 || entry->pause_deadline_us == 0 ||
|
||||
entry->pause_deadline_us <= entry->launch_time_us) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint64_t delay_us = entry->pause_deadline_us - entry->launch_time_us;
|
||||
uint64_t delay_seconds = delay_us / 1000000ull;
|
||||
if (delay_seconds > UINT32_MAX)
|
||||
delay_seconds = UINT32_MAX;
|
||||
return (uint32_t)delay_seconds;
|
||||
}
|
||||
+405
@@ -0,0 +1,405 @@
|
||||
#include "sm_platform.h"
|
||||
#include <pthread.h>
|
||||
|
||||
#include "sm_log.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_types.h"
|
||||
#include "sm_paths.h"
|
||||
|
||||
static sm_error_t g_last_error;
|
||||
static bool g_notifications_initialized = false;
|
||||
static pthread_mutex_t g_notifications_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_mutex_t g_log_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static FILE *g_log_file = NULL;
|
||||
|
||||
extern unsigned char smp_icon_png[];
|
||||
extern unsigned int smp_icon_png_len;
|
||||
|
||||
#define SCE_NOTIFICATION_LOCAL_USER_ID_SYSTEM 0xFE
|
||||
|
||||
int sceNotificationSend(int userId, bool isLogged, const char *payload);
|
||||
int sceNotificationSendById(int userid, bool logged_in, const char *useCaseId,
|
||||
const char *message);
|
||||
|
||||
static void notify_system_plain_message(const char *message) {
|
||||
notify_request_t req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
(void)strlcpy(req.message, message, sizeof(req.message));
|
||||
sceKernelSendNotificationRequest(0, &req, sizeof(req), 0);
|
||||
log_debug("NOTIFY: %s", req.message);
|
||||
}
|
||||
|
||||
static bool ensure_notification_icon_present(void) {
|
||||
struct stat st;
|
||||
if (stat(NOTIFY_ICON_FILE, &st) == 0 && st.st_size > 0)
|
||||
return true;
|
||||
|
||||
(void)mkdir("/user/data", 0777);
|
||||
(void)mkdir(NOTIFY_ICON_DIR, 0777);
|
||||
FILE *fp = fopen(NOTIFY_ICON_FILE, "wb");
|
||||
if (!fp) {
|
||||
log_debug(" [NOTIFY] icon create failed: %s (%s)", NOTIFY_ICON_FILE,
|
||||
strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t written = fwrite(smp_icon_png, 1, smp_icon_png_len, fp);
|
||||
int saved_errno = 0;
|
||||
if (written != smp_icon_png_len)
|
||||
saved_errno = ferror(fp) ? errno : EIO;
|
||||
if (fflush(fp) != 0 && saved_errno == 0)
|
||||
saved_errno = errno;
|
||||
if (fclose(fp) != 0 && saved_errno == 0)
|
||||
saved_errno = errno;
|
||||
|
||||
if (saved_errno != 0) {
|
||||
errno = saved_errno;
|
||||
log_debug(" [NOTIFY] icon write failed: %s (%s)", NOTIFY_ICON_FILE,
|
||||
strerror(errno));
|
||||
(void)unlink(NOTIFY_ICON_FILE);
|
||||
return false;
|
||||
}
|
||||
|
||||
log_debug(" [NOTIFY] icon created: %s", NOTIFY_ICON_FILE);
|
||||
return true;
|
||||
}
|
||||
|
||||
void sm_notifications_init(void) {
|
||||
pthread_mutex_lock(&g_notifications_mutex);
|
||||
if (!g_notifications_initialized)
|
||||
g_notifications_initialized = ensure_notification_icon_present();
|
||||
pthread_mutex_unlock(&g_notifications_mutex);
|
||||
}
|
||||
|
||||
static void append_json_escaped(char *dst, size_t dst_size, const char *src) {
|
||||
size_t used = strlen(dst);
|
||||
if (used >= dst_size)
|
||||
return;
|
||||
|
||||
for (; *src != '\0' && used + 1 < dst_size; ++src) {
|
||||
const char *escape = NULL;
|
||||
char single[2] = {0};
|
||||
|
||||
switch (*src) {
|
||||
case '\\':
|
||||
escape = "\\\\";
|
||||
break;
|
||||
case '"':
|
||||
escape = "\\\"";
|
||||
break;
|
||||
case '\n':
|
||||
escape = "\\n";
|
||||
break;
|
||||
case '\r':
|
||||
escape = "\\r";
|
||||
break;
|
||||
case '\t':
|
||||
escape = "\\t";
|
||||
break;
|
||||
default:
|
||||
single[0] = *src;
|
||||
escape = single;
|
||||
break;
|
||||
}
|
||||
|
||||
size_t escape_len = strlen(escape);
|
||||
if (used + escape_len >= dst_size)
|
||||
break;
|
||||
memcpy(dst + used, escape, escape_len);
|
||||
used += escape_len;
|
||||
dst[used] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
static bool build_notification_timestamp(time_t timestamp, char out[32]) {
|
||||
struct tm tm_utc;
|
||||
if (gmtime_r(×tamp, &tm_utc) == NULL)
|
||||
return false;
|
||||
return strftime(out, 32, "%Y-%m-%dT%H:%M:%S.000Z", &tm_utc) != 0;
|
||||
}
|
||||
|
||||
static bool build_notification_metadata(char *created_at,
|
||||
char *notification_id,
|
||||
size_t notification_id_size) {
|
||||
time_t now;
|
||||
|
||||
now = time(NULL);
|
||||
if (!build_notification_timestamp(now, created_at))
|
||||
return false;
|
||||
|
||||
snprintf(notification_id, notification_id_size, "%u",
|
||||
(unsigned)((uint32_t)now ^ (uint32_t)getpid()));
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool send_rich_notification(const char *message) {
|
||||
char escaped_message[4096];
|
||||
char escaped_version[128];
|
||||
char payload[8192];
|
||||
char created_at[32];
|
||||
char notification_id[32];
|
||||
|
||||
if (!message || message[0] == '\0')
|
||||
return false;
|
||||
|
||||
sm_notifications_init();
|
||||
pthread_mutex_lock(&g_notifications_mutex);
|
||||
bool notifications_ready = g_notifications_initialized;
|
||||
pthread_mutex_unlock(&g_notifications_mutex);
|
||||
if (!notifications_ready)
|
||||
return false;
|
||||
|
||||
escaped_message[0] = '\0';
|
||||
escaped_version[0] = '\0';
|
||||
append_json_escaped(escaped_message, sizeof(escaped_message), message);
|
||||
append_json_escaped(escaped_version, sizeof(escaped_version),
|
||||
SHADOWMOUNT_VERSION);
|
||||
if (!build_notification_metadata(created_at, notification_id,
|
||||
sizeof(notification_id))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int len = snprintf(
|
||||
payload, sizeof(payload),
|
||||
"{\n"
|
||||
" \"rawData\": {\n"
|
||||
" \"viewTemplateType\": \"InteractiveToastTemplateB\",\n"
|
||||
" \"channelType\": \"ServiceFeedback\",\n"
|
||||
" \"bundleName\": \"ShadowMountPlusWelcome\",\n"
|
||||
" \"useCaseId\": \"IDC\",\n"
|
||||
" \"soundEffect\": \"none\",\n"
|
||||
" \"toastOverwriteType\": \"InQueue\",\n"
|
||||
" \"isImmediate\": true,\n"
|
||||
" \"priority\": 100,\n"
|
||||
" \"viewData\": {\n"
|
||||
" \"icon\": {\n"
|
||||
" \"type\": \"Url\",\n"
|
||||
" \"parameters\": {\n"
|
||||
" \"url\": \"" NOTIFY_ICON_FILE "\"\n"
|
||||
" }\n"
|
||||
" },\n"
|
||||
" \"message\": {\n"
|
||||
" \"body\": \"%s\"\n"
|
||||
" },\n"
|
||||
" \"subMessage\": {\n"
|
||||
" \"body\": \"ShadowMountPlus %s\"\n"
|
||||
" },\n"
|
||||
" \"actions\": [\n"
|
||||
" {\n"
|
||||
" \"actionName\": \"Go to Debug Settings\",\n"
|
||||
" \"actionType\": \"DeepLink\",\n"
|
||||
" \"defaultFocus\": true,\n"
|
||||
" \"parameters\": {\n"
|
||||
" \"actionUrl\": \"pssettings:play?function=debug_settings\"\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" ]\n"
|
||||
" },\n"
|
||||
" \"platformViews\": {\n"
|
||||
" \"previewDisabled\": {\n"
|
||||
" \"viewData\": {\n"
|
||||
" \"icon\": {\n"
|
||||
" \"type\": \"Predefined\",\n"
|
||||
" \"parameters\": {\n"
|
||||
" \"icon\": \"community\"\n"
|
||||
" }\n"
|
||||
" },\n"
|
||||
" \"message\": {\n"
|
||||
" \"body\": \"%s\"\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" },\n"
|
||||
" \"createdDateTime\": \"%s\",\n"
|
||||
" \"localNotificationId\": \"%s\"\n"
|
||||
"}",
|
||||
escaped_message, escaped_version, escaped_message, created_at,
|
||||
notification_id);
|
||||
if (len < 0 || (size_t)len >= sizeof(payload))
|
||||
return false;
|
||||
|
||||
int rc = sceNotificationSend(SCE_NOTIFICATION_LOCAL_USER_ID_SYSTEM, true,
|
||||
payload);
|
||||
return rc == 0;
|
||||
}
|
||||
|
||||
static bool send_game_installed_rich_notification(const char *title_id) {
|
||||
char escaped_title_id[128];
|
||||
char params[256];
|
||||
|
||||
if (!title_id || title_id[0] == '\0')
|
||||
return false;
|
||||
|
||||
escaped_title_id[0] = '\0';
|
||||
append_json_escaped(escaped_title_id, sizeof(escaped_title_id), title_id);
|
||||
int len = snprintf(params, sizeof(params),
|
||||
"{\"npTitleId\":\"%s_00\"}", escaped_title_id);
|
||||
if (len < 0 || (size_t)len >= sizeof(params))
|
||||
return false;
|
||||
int rc = sceNotificationSendById(SCE_NOTIFICATION_LOCAL_USER_ID_SYSTEM, true,
|
||||
"NUC240", params);
|
||||
return rc == 0;
|
||||
}
|
||||
|
||||
static void notify_system_plain_v(const char *fmt, va_list args) {
|
||||
char message[3075];
|
||||
vsnprintf(message, sizeof(message), fmt, args);
|
||||
notify_system_plain_message(message);
|
||||
}
|
||||
|
||||
static FILE *ensure_log_file_open_locked(void) {
|
||||
if (g_log_file)
|
||||
return g_log_file;
|
||||
|
||||
(void)mkdir(LOG_DIR, 0777);
|
||||
g_log_file = fopen(LOG_FILE, "a");
|
||||
if (!g_log_file)
|
||||
return NULL;
|
||||
|
||||
return g_log_file;
|
||||
}
|
||||
|
||||
static void log_to_file_locked(const char *fmt, va_list args) {
|
||||
FILE *fp = ensure_log_file_open_locked();
|
||||
if (!fp)
|
||||
return;
|
||||
|
||||
va_list args_file;
|
||||
time_t rawtime;
|
||||
struct tm tm_local;
|
||||
char buffer[80];
|
||||
|
||||
time(&rawtime);
|
||||
(void)localtime_r(&rawtime, &tm_local);
|
||||
(void)strftime(buffer, sizeof(buffer), "%H:%M:%S", &tm_local);
|
||||
|
||||
va_copy(args_file, args);
|
||||
fprintf(fp, "[%s] ", buffer);
|
||||
vfprintf(fp, fmt, args_file);
|
||||
fputc('\n', fp);
|
||||
fflush(fp);
|
||||
va_end(args_file);
|
||||
}
|
||||
|
||||
void log_debug(const char *fmt, ...) {
|
||||
if (runtime_config()->debug_enabled == false)
|
||||
return;
|
||||
|
||||
va_list args;
|
||||
va_list args_copy;
|
||||
va_start(args, fmt);
|
||||
va_copy(args_copy, args);
|
||||
|
||||
pthread_mutex_lock(&g_log_mutex);
|
||||
vfprintf(stdout, fmt, args);
|
||||
fputc('\n', stdout);
|
||||
fflush(stdout);
|
||||
log_to_file_locked(fmt, args_copy);
|
||||
pthread_mutex_unlock(&g_log_mutex);
|
||||
|
||||
va_end(args_copy);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void sm_log_shutdown(void) {
|
||||
pthread_mutex_lock(&g_log_mutex);
|
||||
if (g_log_file) {
|
||||
fclose(g_log_file);
|
||||
g_log_file = NULL;
|
||||
}
|
||||
pthread_mutex_unlock(&g_log_mutex);
|
||||
}
|
||||
|
||||
void notify_system_rich(bool allow_in_quiet_mode, const char *fmt, ...) {
|
||||
char message[3075];
|
||||
va_list args;
|
||||
|
||||
if (runtime_config()->quiet_mode && !allow_in_quiet_mode)
|
||||
return;
|
||||
|
||||
va_start(args, fmt);
|
||||
vsnprintf(message, sizeof(message), fmt, args);
|
||||
va_end(args);
|
||||
|
||||
if (!send_rich_notification(message)) {
|
||||
notify_system_plain_message(message);
|
||||
return;
|
||||
}
|
||||
|
||||
log_debug("NOTIFY: %s", message);
|
||||
}
|
||||
|
||||
void notify_game_installed_rich(const char *title_id) {
|
||||
if (!send_game_installed_rich_notification(title_id)) {
|
||||
notify_system_info("Installed: %s",
|
||||
(title_id && title_id[0] != '\0') ? title_id : "Unknown");
|
||||
return;
|
||||
}
|
||||
|
||||
log_debug("NOTIFY: installed game %s", title_id ? title_id : "unknown");
|
||||
}
|
||||
|
||||
void notify_system_info(const char *fmt, ...) {
|
||||
if (runtime_config()->quiet_mode)
|
||||
return;
|
||||
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
notify_system_plain_v(fmt, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void sm_error_clear(void) {
|
||||
memset(&g_last_error, 0, sizeof(g_last_error));
|
||||
}
|
||||
|
||||
void sm_error_set(const char *subsystem, int code, const char *path,
|
||||
const char *fmt, ...) {
|
||||
memset(&g_last_error, 0, sizeof(g_last_error));
|
||||
g_last_error.valid = true;
|
||||
g_last_error.code = code;
|
||||
if (subsystem && subsystem[0] != '\0')
|
||||
(void)strlcpy(g_last_error.subsystem, subsystem,
|
||||
sizeof(g_last_error.subsystem));
|
||||
if (path && path[0] != '\0')
|
||||
(void)strlcpy(g_last_error.path, path, sizeof(g_last_error.path));
|
||||
if (fmt && fmt[0] != '\0') {
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
vsnprintf(g_last_error.message, sizeof(g_last_error.message), fmt, args);
|
||||
va_end(args);
|
||||
}
|
||||
}
|
||||
|
||||
const sm_error_t *sm_last_error(void) {
|
||||
return &g_last_error;
|
||||
}
|
||||
|
||||
bool sm_error_notified(void) {
|
||||
return g_last_error.notified;
|
||||
}
|
||||
|
||||
void sm_error_mark_notified(void) {
|
||||
g_last_error.notified = true;
|
||||
}
|
||||
|
||||
void notify_system(const char *fmt, ...) {
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
notify_system_plain_v(fmt, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void notify_image_mount_failed(const char *path, int mount_err) {
|
||||
if (g_last_error.valid && g_last_error.message[0] != '\0') {
|
||||
const char *error_path =
|
||||
(g_last_error.path[0] != '\0') ? g_last_error.path : path;
|
||||
notify_system("%s\n%s", g_last_error.message, error_path);
|
||||
g_last_error.notified = true;
|
||||
return;
|
||||
}
|
||||
|
||||
notify_system("Image mount failed: 0x%08X (%s)\n%s", (uint32_t)mount_err,
|
||||
strerror(mount_err), path);
|
||||
}
|
||||
+420
@@ -0,0 +1,420 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include "sm_manual.h"
|
||||
|
||||
#include "sm_appdb.h"
|
||||
#include "sm_install_queue.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_paths.h"
|
||||
#include "sm_title_state.h"
|
||||
|
||||
#define MANUAL_STATUS_CAPACITY 1024
|
||||
|
||||
typedef enum {
|
||||
MANUAL_STATUS_UNKNOWN = 0,
|
||||
MANUAL_STATUS_INSTALLED,
|
||||
MANUAL_STATUS_DELETED,
|
||||
} manual_status_t;
|
||||
|
||||
typedef struct {
|
||||
char path[MAX_PATH];
|
||||
char title_id[MAX_TITLE_ID];
|
||||
char title_name[MAX_TITLE_NAME];
|
||||
manual_status_t status;
|
||||
bool remove_from_list;
|
||||
} manual_status_entry_t;
|
||||
|
||||
static manual_status_entry_t g_manual_status_entries[MANUAL_STATUS_CAPACITY];
|
||||
|
||||
static void trim_manual_line(char *line) {
|
||||
if (!line)
|
||||
return;
|
||||
|
||||
char *start = line;
|
||||
while (*start != '\0' && isspace((unsigned char)*start))
|
||||
start++;
|
||||
if (start != line)
|
||||
memmove(line, start, strlen(start) + 1u);
|
||||
|
||||
size_t len = strlen(line);
|
||||
while (len > 0 && isspace((unsigned char)line[len - 1u])) {
|
||||
line[len - 1u] = '\0';
|
||||
len--;
|
||||
}
|
||||
while (len > 1 && line[len - 1u] == '/') {
|
||||
line[len - 1u] = '\0';
|
||||
len--;
|
||||
}
|
||||
}
|
||||
|
||||
static void drain_manual_file_line(FILE *f) {
|
||||
int ch;
|
||||
while ((ch = fgetc(f)) != EOF && ch != '\n') {
|
||||
}
|
||||
}
|
||||
|
||||
static void copy_manual_file_line_remainder(FILE *in, FILE *out,
|
||||
int *saved_errno) {
|
||||
int ch;
|
||||
while ((ch = fgetc(in)) != EOF) {
|
||||
if (saved_errno && *saved_errno == 0 && fputc(ch, out) == EOF)
|
||||
*saved_errno = errno;
|
||||
if (ch == '\n')
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool sm_manual_for_each_path(bool (*visit)(const char *path, void *ctx),
|
||||
void *ctx) {
|
||||
if (!visit)
|
||||
return false;
|
||||
|
||||
FILE *f = fopen(MANUAL_LIST_FILE, "r");
|
||||
if (!f) {
|
||||
if (errno != ENOENT)
|
||||
log_debug(" [MANUAL] open failed for %s: %s", MANUAL_LIST_FILE,
|
||||
strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
char line[MAX_PATH + 64];
|
||||
bool completed = true;
|
||||
while (fgets(line, sizeof(line), f) != NULL) {
|
||||
bool truncated =
|
||||
strchr(line, '\n') == NULL && strlen(line) == sizeof(line) - 1u;
|
||||
trim_manual_line(line);
|
||||
if (line[0] == '\0' || line[0] == '#') {
|
||||
if (truncated)
|
||||
drain_manual_file_line(f);
|
||||
continue;
|
||||
}
|
||||
if (truncated || strlen(line) >= MAX_PATH) {
|
||||
log_debug(" [MANUAL] path too long, ignored: %.80s", line);
|
||||
if (truncated)
|
||||
drain_manual_file_line(f);
|
||||
continue;
|
||||
}
|
||||
if (!visit(line, ctx)) {
|
||||
completed = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ferror(f)) {
|
||||
log_debug(" [MANUAL] read failed for %s: %s", MANUAL_LIST_FILE,
|
||||
strerror(errno));
|
||||
completed = false;
|
||||
}
|
||||
fclose(f);
|
||||
return completed;
|
||||
}
|
||||
|
||||
static const char *manual_status_name(manual_status_t status) {
|
||||
switch (status) {
|
||||
case MANUAL_STATUS_INSTALLED:
|
||||
return "installed";
|
||||
case MANUAL_STATUS_DELETED:
|
||||
return "deleted";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static manual_status_t parse_manual_status_name(const char *name) {
|
||||
if (!name)
|
||||
return MANUAL_STATUS_UNKNOWN;
|
||||
if (strcasecmp(name, "installed") == 0)
|
||||
return MANUAL_STATUS_INSTALLED;
|
||||
if (strcasecmp(name, "deleted") == 0)
|
||||
return MANUAL_STATUS_DELETED;
|
||||
return MANUAL_STATUS_UNKNOWN;
|
||||
}
|
||||
|
||||
static bool parse_manual_status_line(char *line, manual_status_entry_t *entry) {
|
||||
char *status = strtok(line, "\t");
|
||||
char *title_id = strtok(NULL, "\t");
|
||||
char *path = strtok(NULL, "\t");
|
||||
char *title_name = strtok(NULL, "\n");
|
||||
if (!status || !title_id || !path)
|
||||
return false;
|
||||
|
||||
trim_manual_line(status);
|
||||
trim_manual_line(title_id);
|
||||
trim_manual_line(path);
|
||||
if (title_name)
|
||||
trim_manual_line(title_name);
|
||||
|
||||
manual_status_t parsed_status = parse_manual_status_name(status);
|
||||
if (parsed_status == MANUAL_STATUS_UNKNOWN || title_id[0] == '\0' ||
|
||||
path[0] == '\0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
memset(entry, 0, sizeof(*entry));
|
||||
entry->status = parsed_status;
|
||||
(void)strlcpy(entry->title_id, title_id, sizeof(entry->title_id));
|
||||
(void)strlcpy(entry->path, path, sizeof(entry->path));
|
||||
if (title_name && title_name[0] != '\0')
|
||||
(void)strlcpy(entry->title_name, title_name, sizeof(entry->title_name));
|
||||
return true;
|
||||
}
|
||||
|
||||
static int load_manual_status(void) {
|
||||
FILE *f = fopen(MANUAL_STATUS_FILE, "r");
|
||||
if (!f) {
|
||||
if (errno != ENOENT)
|
||||
log_debug(" [MANUAL] status open failed for %s: %s",
|
||||
MANUAL_STATUS_FILE, strerror(errno));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
char line[MAX_PATH + MAX_TITLE_NAME + MAX_TITLE_ID + 64];
|
||||
while (fgets(line, sizeof(line), f) != NULL) {
|
||||
if (count >= MANUAL_STATUS_CAPACITY) {
|
||||
log_debug(" [MANUAL] status limit reached (%d)",
|
||||
MANUAL_STATUS_CAPACITY);
|
||||
break;
|
||||
}
|
||||
manual_status_entry_t entry;
|
||||
if (!parse_manual_status_line(line, &entry))
|
||||
continue;
|
||||
g_manual_status_entries[count++] = entry;
|
||||
}
|
||||
|
||||
if (ferror(f))
|
||||
log_debug(" [MANUAL] status read failed for %s: %s",
|
||||
MANUAL_STATUS_FILE, strerror(errno));
|
||||
fclose(f);
|
||||
return count;
|
||||
}
|
||||
|
||||
static bool write_manual_status(int entry_count) {
|
||||
mkdir(LOG_DIR, 0777);
|
||||
|
||||
char tmp_path[MAX_PATH];
|
||||
snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", MANUAL_STATUS_FILE);
|
||||
FILE *f = fopen(tmp_path, "w");
|
||||
if (!f) {
|
||||
log_debug(" [MANUAL] status write open failed for %s: %s", tmp_path,
|
||||
strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
int saved_errno = 0;
|
||||
for (int i = 0; i < entry_count; i++) {
|
||||
const manual_status_entry_t *entry = &g_manual_status_entries[i];
|
||||
if (entry->status == MANUAL_STATUS_UNKNOWN || entry->path[0] == '\0' ||
|
||||
entry->title_id[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
if (fprintf(f, "%s\t%s\t%s\t%s\n", manual_status_name(entry->status),
|
||||
entry->title_id, entry->path, entry->title_name) < 0) {
|
||||
saved_errno = errno;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (fflush(f) != 0 && saved_errno == 0)
|
||||
saved_errno = errno;
|
||||
if (fclose(f) != 0 && saved_errno == 0)
|
||||
saved_errno = errno;
|
||||
if (saved_errno != 0) {
|
||||
errno = saved_errno;
|
||||
log_debug(" [MANUAL] status write failed for %s: %s", tmp_path,
|
||||
strerror(errno));
|
||||
(void)unlink(tmp_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rename(tmp_path, MANUAL_STATUS_FILE) != 0) {
|
||||
log_debug(" [MANUAL] status rename failed for %s: %s",
|
||||
MANUAL_STATUS_FILE, strerror(errno));
|
||||
(void)unlink(tmp_path);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static int find_manual_status_entry(int entry_count, const char *path,
|
||||
const char *title_id) {
|
||||
for (int i = 0; i < entry_count; i++) {
|
||||
if (path && path[0] != '\0' &&
|
||||
strcmp(g_manual_status_entries[i].path, path) == 0) {
|
||||
return i;
|
||||
}
|
||||
if (title_id && title_id[0] != '\0' &&
|
||||
strcmp(g_manual_status_entries[i].title_id, title_id) == 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static bool rewrite_manual_list_without_deleted_entries(int entry_count) {
|
||||
FILE *in = fopen(MANUAL_LIST_FILE, "r");
|
||||
if (!in) {
|
||||
if (errno == ENOENT)
|
||||
return true;
|
||||
log_debug(" [MANUAL] list rewrite open failed for %s: %s",
|
||||
MANUAL_LIST_FILE, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
char tmp_path[MAX_PATH];
|
||||
snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", MANUAL_LIST_FILE);
|
||||
FILE *out = fopen(tmp_path, "w");
|
||||
if (!out) {
|
||||
log_debug(" [MANUAL] list rewrite temp open failed for %s: %s", tmp_path,
|
||||
strerror(errno));
|
||||
fclose(in);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
int saved_errno = 0;
|
||||
char line[MAX_PATH + 64];
|
||||
while (fgets(line, sizeof(line), in) != NULL) {
|
||||
bool truncated =
|
||||
strchr(line, '\n') == NULL && strlen(line) == sizeof(line) - 1u;
|
||||
char normalized[MAX_PATH + 64];
|
||||
(void)strlcpy(normalized, line, sizeof(normalized));
|
||||
trim_manual_line(normalized);
|
||||
if (!truncated && normalized[0] != '\0' && normalized[0] != '#') {
|
||||
bool remove_line = false;
|
||||
for (int i = 0; i < entry_count; i++) {
|
||||
if (g_manual_status_entries[i].remove_from_list &&
|
||||
strcmp(g_manual_status_entries[i].path, normalized) == 0) {
|
||||
remove_line = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (remove_line) {
|
||||
changed = true;
|
||||
if (truncated)
|
||||
drain_manual_file_line(in);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (fputs(line, out) == EOF) {
|
||||
saved_errno = errno;
|
||||
break;
|
||||
}
|
||||
if (truncated)
|
||||
copy_manual_file_line_remainder(in, out, &saved_errno);
|
||||
}
|
||||
if (ferror(in) && saved_errno == 0)
|
||||
saved_errno = errno;
|
||||
if (fflush(out) != 0 && saved_errno == 0)
|
||||
saved_errno = errno;
|
||||
if (fclose(out) != 0 && saved_errno == 0)
|
||||
saved_errno = errno;
|
||||
fclose(in);
|
||||
|
||||
if (saved_errno != 0) {
|
||||
errno = saved_errno;
|
||||
log_debug(" [MANUAL] list rewrite failed for %s: %s", MANUAL_LIST_FILE,
|
||||
strerror(errno));
|
||||
(void)unlink(tmp_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
(void)unlink(tmp_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (rename(tmp_path, MANUAL_LIST_FILE) != 0) {
|
||||
log_debug(" [MANUAL] list rewrite rename failed for %s: %s",
|
||||
MANUAL_LIST_FILE, strerror(errno));
|
||||
(void)unlink(tmp_path);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool sm_manual_reconcile_deleted_titles(
|
||||
const struct AppDbTitleList *app_db_titles, bool app_db_titles_ready) {
|
||||
if (!app_db_titles_ready || !app_db_titles)
|
||||
return true;
|
||||
|
||||
int entry_count = load_manual_status();
|
||||
if (entry_count <= 0)
|
||||
return true;
|
||||
|
||||
bool changed = false;
|
||||
|
||||
for (int i = 0; i < entry_count; i++) {
|
||||
manual_status_entry_t *entry = &g_manual_status_entries[i];
|
||||
if (entry->status != MANUAL_STATUS_INSTALLED ||
|
||||
entry->title_id[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
if (app_db_title_list_contains(app_db_titles, entry->title_id))
|
||||
continue;
|
||||
|
||||
entry->status = MANUAL_STATUS_DELETED;
|
||||
entry->remove_from_list = true;
|
||||
sm_install_forget_pending_title(entry->title_id);
|
||||
clear_register_attempts(entry->title_id);
|
||||
clear_failed_mount_attempts(entry->title_id);
|
||||
changed = true;
|
||||
log_debug(" [MANUAL] title removed from app.db, disabling manual entry: "
|
||||
"%s (%s)",
|
||||
entry->title_name, entry->title_id);
|
||||
}
|
||||
|
||||
if (!changed)
|
||||
return true;
|
||||
|
||||
if (!rewrite_manual_list_without_deleted_entries(entry_count))
|
||||
return false;
|
||||
return write_manual_status(entry_count);
|
||||
}
|
||||
|
||||
void sm_manual_note_installed(const char *manual_source_path,
|
||||
const char *title_id,
|
||||
const char *title_name) {
|
||||
if (!manual_source_path || manual_source_path[0] == '\0' || !title_id ||
|
||||
title_id[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
int entry_count = load_manual_status();
|
||||
int index =
|
||||
find_manual_status_entry(entry_count, manual_source_path, title_id);
|
||||
if (index < 0) {
|
||||
if (entry_count >= MANUAL_STATUS_CAPACITY) {
|
||||
log_debug(" [MANUAL] status full, cannot record %s (%s)",
|
||||
manual_source_path, title_id);
|
||||
return;
|
||||
}
|
||||
index = entry_count++;
|
||||
memset(&g_manual_status_entries[index], 0,
|
||||
sizeof(g_manual_status_entries[index]));
|
||||
}
|
||||
|
||||
manual_status_entry_t *entry = &g_manual_status_entries[index];
|
||||
manual_status_t previous_status = entry->status;
|
||||
bool unchanged = previous_status == MANUAL_STATUS_INSTALLED &&
|
||||
strcmp(entry->path, manual_source_path) == 0 &&
|
||||
strcmp(entry->title_id, title_id) == 0 &&
|
||||
(!title_name || title_name[0] == '\0' ||
|
||||
strcmp(entry->title_name, title_name) == 0);
|
||||
if (unchanged)
|
||||
return;
|
||||
|
||||
entry->status = MANUAL_STATUS_INSTALLED;
|
||||
(void)strlcpy(entry->path, manual_source_path, sizeof(entry->path));
|
||||
(void)strlcpy(entry->title_id, title_id, sizeof(entry->title_id));
|
||||
if (title_name && title_name[0] != '\0')
|
||||
(void)strlcpy(entry->title_name, title_name, sizeof(entry->title_name));
|
||||
|
||||
if (!write_manual_status(entry_count))
|
||||
return;
|
||||
if (previous_status != MANUAL_STATUS_INSTALLED) {
|
||||
log_debug(" [MANUAL] installed status recorded: %s (%s) from %s",
|
||||
entry->title_name, entry->title_id, entry->path);
|
||||
}
|
||||
}
|
||||
+664
@@ -0,0 +1,664 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_mdbg.h"
|
||||
#include "sm_time.h"
|
||||
#include "sm_types.h"
|
||||
|
||||
int sceKernelDebugGetPrivateLogText(void *buffer, size_t buffer_size,
|
||||
char **text, uint64_t *text_size);
|
||||
int sceKernelDebugGetSdkLogText(void *buffer, size_t buffer_size,
|
||||
char **text, uint64_t *text_size);
|
||||
uint64_t sceKernelDebugGetLogBufferSize(void) __asm__("C49jelxiaVE");
|
||||
int mdbg_call(void *cmd, void *req, void *res);
|
||||
|
||||
#define MDBG_CMD_TYPE_SERVICE 1ull
|
||||
#define MDBG_CMD_PROCESS_STATE 30ull
|
||||
#define MDBG_SUBCMD_FLAGS 2ull
|
||||
|
||||
#define MDBG_FLAG_EXCEPTION_STOP 0x00080000ull
|
||||
#define MDBG_AUTOTUNE_WINDOW_US (300ull * 1000000ull)
|
||||
#define MDBG_LOG_LINE_BUFFER_SIZE 512u
|
||||
#define MDBG_RTLD_ERROR_PREFIX_SIZE 32u
|
||||
#ifndef MDBG_USE_PRIVATE_LOG_TEXT
|
||||
#define MDBG_USE_PRIVATE_LOG_TEXT 0
|
||||
#endif
|
||||
#ifndef MDBG_SKIP_PRIVILEGE_ELEVATION
|
||||
#define MDBG_SKIP_PRIVILEGE_ELEVATION 0
|
||||
#endif
|
||||
|
||||
#if MDBG_USE_PRIVATE_LOG_TEXT
|
||||
#define MDBG_FETCH_LOG_TEXT sceKernelDebugGetPrivateLogText
|
||||
#else
|
||||
#define MDBG_FETCH_LOG_TEXT sceKernelDebugGetSdkLogText
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
uint64_t type;
|
||||
uint64_t cmd;
|
||||
} mdbg_cmd_t;
|
||||
|
||||
typedef struct {
|
||||
int64_t pid;
|
||||
int64_t subcmd;
|
||||
uint64_t arg;
|
||||
uint64_t reserved[5];
|
||||
} mdbg_req_t;
|
||||
|
||||
typedef struct {
|
||||
int64_t status;
|
||||
uint64_t value;
|
||||
uint64_t reserved[2];
|
||||
} mdbg_res_t;
|
||||
|
||||
typedef struct {
|
||||
bool active;
|
||||
bool log_monitoring_active;
|
||||
bool pause_seen;
|
||||
pid_t pid;
|
||||
uint32_t pause_delay_seconds;
|
||||
uint64_t monitor_deadline_us;
|
||||
uint64_t pause_time_us;
|
||||
uint64_t next_poll_us;
|
||||
char title_id[MAX_TITLE_ID];
|
||||
char rtld_error_prefix[MDBG_RTLD_ERROR_PREFIX_SIZE];
|
||||
} mdbg_game_state_t;
|
||||
|
||||
typedef struct {
|
||||
bool privilege_probe_done;
|
||||
bool privilege_ready;
|
||||
size_t log_buffer_size;
|
||||
size_t log_snapshot_length;
|
||||
size_t log_line_length;
|
||||
char *log_snapshot;
|
||||
char *log_storage;
|
||||
char log_line[MDBG_LOG_LINE_BUFFER_SIZE];
|
||||
mdbg_game_state_t game;
|
||||
} sm_mdbg_state_t;
|
||||
|
||||
static sm_mdbg_state_t g_mdbg;
|
||||
|
||||
static bool sm_mdbg_enabled(void);
|
||||
#if !MDBG_SKIP_PRIVILEGE_ELEVATION
|
||||
static int elevate_to_coredump(void);
|
||||
#endif
|
||||
static bool ensure_mdbg_privileges(void);
|
||||
static int mdbg_call_raw(int64_t pid, uint64_t subcmd, uint64_t arg,
|
||||
int64_t *status_out, uint64_t *value_out);
|
||||
static int query_mdbg_flags(pid_t pid, uint64_t *flags_out);
|
||||
static bool is_mdbg_process_gone_error(int ret);
|
||||
static void reset_log_line_buffer(void);
|
||||
static void reset_log_snapshot(void);
|
||||
static void free_log_buffers(void);
|
||||
static bool ensure_log_buffers(void);
|
||||
static int fetch_log_text(const char **text_out, size_t *text_len_out);
|
||||
static size_t find_log_overlap(const char *current, size_t current_len);
|
||||
static void update_log_snapshot(const char *text, size_t text_len);
|
||||
static void clear_tracked_game(void);
|
||||
static bool matches_tracked_rtld_error(const char *text);
|
||||
static void summarize_failure_reason(const char *reason, char *summary_out,
|
||||
size_t summary_out_size,
|
||||
bool is_rtld_error);
|
||||
static void handle_pre_pause_failure(const char *reason);
|
||||
static void handle_post_pause_failure(const char *reason, uint64_t now_us);
|
||||
static void process_log_line(const char *line, uint64_t now_us);
|
||||
static void append_log_char(char ch, uint64_t now_us);
|
||||
static void poll_log_monitor(uint64_t now_us);
|
||||
static void start_log_monitoring(void);
|
||||
static void handle_crash_candidate(uint64_t flags, uint64_t now_us);
|
||||
|
||||
static bool sm_mdbg_enabled(void) {
|
||||
return runtime_config()->kstuff_crash_detection_enabled &&
|
||||
runtime_config()->kstuff_game_auto_toggle;
|
||||
}
|
||||
|
||||
#if !MDBG_SKIP_PRIVILEGE_ELEVATION
|
||||
#define SCE_AUTHID_COREDUMP 0x4800000000000006ull
|
||||
static int elevate_to_coredump(void) {
|
||||
static const uint8_t k_priv_caps[16] = {
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
|
||||
pid_t pid = getpid();
|
||||
|
||||
if (kernel_set_ucred_authid(pid, SCE_AUTHID_COREDUMP) < 0)
|
||||
return -1;
|
||||
if (kernel_set_ucred_caps(pid, k_priv_caps) < 0)
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
static bool ensure_mdbg_privileges(void) {
|
||||
if (g_mdbg.privilege_probe_done)
|
||||
return g_mdbg.privilege_ready;
|
||||
|
||||
g_mdbg.privilege_probe_done = true;
|
||||
#if MDBG_SKIP_PRIVILEGE_ELEVATION
|
||||
g_mdbg.privilege_ready = true;
|
||||
log_debug(" [MDBG] coredump privilege probe skipped");
|
||||
#else
|
||||
g_mdbg.privilege_ready = elevate_to_coredump() == 0;
|
||||
if (g_mdbg.privilege_ready) {
|
||||
log_debug(" [MDBG] coredump privileges enabled");
|
||||
} else {
|
||||
log_debug(" [MDBG] failed to enable coredump privileges; mdbg polling "
|
||||
"disabled, log monitoring disabled");
|
||||
}
|
||||
#endif
|
||||
|
||||
return g_mdbg.privilege_ready;
|
||||
}
|
||||
|
||||
static int mdbg_call_raw(int64_t pid, uint64_t subcmd, uint64_t arg,
|
||||
int64_t *status_out, uint64_t *value_out) {
|
||||
mdbg_cmd_t cmd = {MDBG_CMD_TYPE_SERVICE, MDBG_CMD_PROCESS_STATE};
|
||||
mdbg_req_t req = {
|
||||
.pid = pid,
|
||||
.subcmd = (int64_t)subcmd,
|
||||
.arg = arg,
|
||||
};
|
||||
mdbg_res_t res = {0};
|
||||
int ret = mdbg_call(&cmd, &req, &res);
|
||||
if (ret != 0)
|
||||
return ret;
|
||||
|
||||
if (status_out)
|
||||
*status_out = res.status;
|
||||
if (value_out)
|
||||
*value_out = res.value;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int query_mdbg_flags(pid_t pid, uint64_t *flags_out) {
|
||||
int64_t status;
|
||||
uint64_t value;
|
||||
int ret = mdbg_call_raw(pid, MDBG_SUBCMD_FLAGS, 0, &status, &value);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
if (status != 0)
|
||||
return (int)status;
|
||||
if (flags_out)
|
||||
*flags_out = value;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool is_mdbg_process_gone_error(int ret) {
|
||||
return ret == -ESRCH || ret == -ENOENT || ret == ESRCH || ret == ENOENT;
|
||||
}
|
||||
|
||||
static void reset_log_line_buffer(void) {
|
||||
g_mdbg.log_line_length = 0;
|
||||
g_mdbg.log_line[0] = '\0';
|
||||
}
|
||||
|
||||
static void reset_log_snapshot(void) {
|
||||
g_mdbg.log_snapshot_length = 0;
|
||||
reset_log_line_buffer();
|
||||
}
|
||||
|
||||
static void free_log_buffers(void) {
|
||||
free(g_mdbg.log_snapshot);
|
||||
free(g_mdbg.log_storage);
|
||||
g_mdbg.log_snapshot = NULL;
|
||||
g_mdbg.log_storage = NULL;
|
||||
g_mdbg.log_buffer_size = 0;
|
||||
reset_log_snapshot();
|
||||
}
|
||||
|
||||
static bool ensure_log_buffers(void) {
|
||||
if (g_mdbg.log_storage && g_mdbg.log_snapshot && g_mdbg.log_buffer_size != 0)
|
||||
return true;
|
||||
|
||||
free_log_buffers();
|
||||
|
||||
const char *title_id =
|
||||
g_mdbg.game.title_id[0] != '\0' ? g_mdbg.game.title_id : "?";
|
||||
uint64_t raw_size = sceKernelDebugGetLogBufferSize();
|
||||
if (raw_size == 0) {
|
||||
log_debug(" [MDBG] invalid log buffer size for %s: 0x%016" PRIx64,
|
||||
title_id,
|
||||
raw_size);
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t buffer_size = (size_t)raw_size;
|
||||
char *storage = malloc(buffer_size);
|
||||
char *snapshot = malloc(buffer_size);
|
||||
if (!storage || !snapshot) {
|
||||
free(storage);
|
||||
free(snapshot);
|
||||
log_debug(" [MDBG] failed to allocate log buffers for %s: size=0x%zx",
|
||||
title_id,
|
||||
buffer_size);
|
||||
return false;
|
||||
}
|
||||
|
||||
g_mdbg.log_storage = storage;
|
||||
g_mdbg.log_snapshot = snapshot;
|
||||
g_mdbg.log_buffer_size = buffer_size;
|
||||
reset_log_snapshot();
|
||||
log_debug(" [MDBG] log monitor ready: %s buffer_size=0x%zx", title_id,
|
||||
buffer_size);
|
||||
return true;
|
||||
}
|
||||
|
||||
static int fetch_log_text(const char **text_out, size_t *text_len_out) {
|
||||
char *raw_text = NULL;
|
||||
uint64_t raw_len = 0;
|
||||
|
||||
*text_out = NULL;
|
||||
*text_len_out = 0;
|
||||
|
||||
int ret =
|
||||
MDBG_FETCH_LOG_TEXT(g_mdbg.log_storage, g_mdbg.log_buffer_size, &raw_text,
|
||||
&raw_len);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
|
||||
if (!raw_text || raw_len == 0) {
|
||||
return 0;
|
||||
}
|
||||
if (raw_len > g_mdbg.log_buffer_size)
|
||||
return -EOVERFLOW;
|
||||
|
||||
*text_out = raw_text;
|
||||
*text_len_out = (size_t)raw_len;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static size_t find_log_overlap(const char *current, size_t current_len) {
|
||||
if (!g_mdbg.log_snapshot || g_mdbg.log_snapshot_length == 0 || !current ||
|
||||
current_len == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t max_overlap = g_mdbg.log_snapshot_length < current_len
|
||||
? g_mdbg.log_snapshot_length
|
||||
: current_len;
|
||||
|
||||
const char *search = g_mdbg.log_snapshot + g_mdbg.log_snapshot_length -
|
||||
max_overlap;
|
||||
const char *end = g_mdbg.log_snapshot + g_mdbg.log_snapshot_length;
|
||||
while (search < end) {
|
||||
const char *candidate =
|
||||
memchr(search, current[0], (size_t)(end - search));
|
||||
if (!candidate)
|
||||
break;
|
||||
|
||||
size_t overlap = (size_t)(end - candidate);
|
||||
if (!memcmp(candidate, current, overlap))
|
||||
return overlap;
|
||||
|
||||
search = candidate + 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void update_log_snapshot(const char *text, size_t text_len) {
|
||||
if (!g_mdbg.log_snapshot || g_mdbg.log_buffer_size == 0)
|
||||
return;
|
||||
|
||||
if (!text || text_len == 0) {
|
||||
g_mdbg.log_snapshot_length = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (text_len > g_mdbg.log_buffer_size)
|
||||
text_len = g_mdbg.log_buffer_size;
|
||||
|
||||
memcpy(g_mdbg.log_snapshot, text, text_len);
|
||||
g_mdbg.log_snapshot_length = text_len;
|
||||
}
|
||||
|
||||
static void clear_tracked_game(void) {
|
||||
memset(&g_mdbg.game, 0, sizeof(g_mdbg.game));
|
||||
free_log_buffers();
|
||||
}
|
||||
|
||||
static bool matches_tracked_rtld_error(const char *text) {
|
||||
return text &&
|
||||
g_mdbg.game.rtld_error_prefix[0] != '\0' &&
|
||||
strstr(text, g_mdbg.game.rtld_error_prefix) != NULL;
|
||||
}
|
||||
|
||||
static void summarize_failure_reason(const char *reason, char *summary_out,
|
||||
size_t summary_out_size,
|
||||
bool is_rtld_error) {
|
||||
if (!summary_out || summary_out_size == 0)
|
||||
return;
|
||||
|
||||
summary_out[0] = '\0';
|
||||
if (!reason || reason[0] == '\0')
|
||||
return;
|
||||
|
||||
if (is_rtld_error) {
|
||||
const char *open_paren = strrchr(reason, '(');
|
||||
const char *close_paren =
|
||||
open_paren ? strchr(open_paren + 1, ')') : NULL;
|
||||
if (open_paren && close_paren && close_paren > open_paren + 1) {
|
||||
int written = snprintf(summary_out, summary_out_size,
|
||||
"can't load module %.*s after KStuff pause",
|
||||
(int)(close_paren - open_paren - 1),
|
||||
open_paren + 1);
|
||||
if (written > 0 && (size_t)written < summary_out_size)
|
||||
return;
|
||||
}
|
||||
|
||||
(void)strlcpy(summary_out, "can't load module after KStuff pause",
|
||||
summary_out_size);
|
||||
return;
|
||||
}
|
||||
|
||||
(void)strlcpy(summary_out, reason, summary_out_size);
|
||||
}
|
||||
|
||||
static void handle_pre_pause_failure(const char *reason) {
|
||||
log_debug(" [MDBG] %s crashed before kstuff auto-pause%s%s",
|
||||
g_mdbg.game.title_id, reason ? ": " : "", reason ? reason : "");
|
||||
notify_system_info("App crashed before KStuff pause: %s. KStuff is not to "
|
||||
"blame.",
|
||||
g_mdbg.game.title_id);
|
||||
clear_tracked_game();
|
||||
}
|
||||
|
||||
static void handle_post_pause_failure(const char *reason, uint64_t now_us) {
|
||||
if (!g_mdbg.game.pause_seen || g_mdbg.game.pause_time_us == 0 ||
|
||||
now_us < g_mdbg.game.pause_time_us) {
|
||||
handle_pre_pause_failure(reason);
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t post_pause_us = now_us - g_mdbg.game.pause_time_us;
|
||||
if (post_pause_us > MDBG_AUTOTUNE_WINDOW_US) {
|
||||
log_debug(" [MDBG] %s crashed %us after kstuff pause; autotune skipped%s%s",
|
||||
g_mdbg.game.title_id, (unsigned)(post_pause_us / 1000000ull),
|
||||
reason ? ": " : "", reason ? reason : "");
|
||||
notify_system_info("App crashed after KStuff pause: %s. Autotune skipped.",
|
||||
g_mdbg.game.title_id);
|
||||
clear_tracked_game();
|
||||
return;
|
||||
}
|
||||
|
||||
char reason_summary[128];
|
||||
bool is_rtld_error = matches_tracked_rtld_error(reason);
|
||||
summarize_failure_reason(reason, reason_summary, sizeof(reason_summary),
|
||||
is_rtld_error);
|
||||
|
||||
uint32_t tuned_delay_seconds = 0;
|
||||
if (upsert_kstuff_autotune_pause_delay(g_mdbg.game.title_id,
|
||||
g_mdbg.game.pause_delay_seconds,
|
||||
&tuned_delay_seconds)) {
|
||||
log_debug(" [MDBG] autotune pause delay updated: %s=%us",
|
||||
g_mdbg.game.title_id, tuned_delay_seconds);
|
||||
if (reason_summary[0] != '\0')
|
||||
log_debug(" [MDBG] autotune trigger: %s", reason_summary);
|
||||
if (is_rtld_error) {
|
||||
notify_system_info("%s: %s. Delay increased to %us.\nLaunch the game again.",
|
||||
g_mdbg.game.title_id,
|
||||
reason_summary[0] != '\0' ? reason_summary
|
||||
: "Can't load module after "
|
||||
"KStuff pause",
|
||||
tuned_delay_seconds);
|
||||
} else {
|
||||
notify_system_info("Crash detected after KStuff pause: pause delay for %s "
|
||||
"increased to %us. Launch the game again.",
|
||||
g_mdbg.game.title_id, tuned_delay_seconds);
|
||||
}
|
||||
clear_tracked_game();
|
||||
return;
|
||||
}
|
||||
|
||||
log_debug(" [MDBG] failed to persist autotune pause delay for %s%s%s",
|
||||
g_mdbg.game.title_id, reason ? ": " : "", reason ? reason : "");
|
||||
notify_system_info("Crash detected after KStuff pause: %s. Failed to update "
|
||||
"autotune delay.",
|
||||
g_mdbg.game.title_id);
|
||||
clear_tracked_game();
|
||||
}
|
||||
|
||||
static void process_log_line(const char *line, uint64_t now_us) {
|
||||
if (!matches_tracked_rtld_error(line))
|
||||
return;
|
||||
|
||||
log_debug(" [MDBG] log load error for %s: %s", g_mdbg.game.title_id, line);
|
||||
handle_post_pause_failure(line, now_us);
|
||||
}
|
||||
|
||||
static void append_log_char(char ch, uint64_t now_us) {
|
||||
if (!g_mdbg.game.active)
|
||||
return;
|
||||
|
||||
if (ch == '\r' || ch == '\n') {
|
||||
if (g_mdbg.log_line_length == 0)
|
||||
return;
|
||||
g_mdbg.log_line[g_mdbg.log_line_length] = '\0';
|
||||
process_log_line(g_mdbg.log_line, now_us);
|
||||
reset_log_line_buffer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_mdbg.log_line_length + 1u >= sizeof(g_mdbg.log_line)) {
|
||||
g_mdbg.log_line[g_mdbg.log_line_length] = '\0';
|
||||
process_log_line(g_mdbg.log_line, now_us);
|
||||
reset_log_line_buffer();
|
||||
}
|
||||
|
||||
g_mdbg.log_line[g_mdbg.log_line_length++] = ch;
|
||||
}
|
||||
|
||||
static void poll_log_monitor(uint64_t now_us) {
|
||||
if (!g_mdbg.game.active || !g_mdbg.game.log_monitoring_active) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ensure_log_buffers()) {
|
||||
g_mdbg.game.log_monitoring_active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const char *text = NULL;
|
||||
size_t text_len = 0;
|
||||
int ret = fetch_log_text(&text, &text_len);
|
||||
if (ret < 0) {
|
||||
log_debug(" [MDBG] log snapshot failed for %s: 0x%08x",
|
||||
g_mdbg.game.title_id, ret);
|
||||
g_mdbg.game.log_monitoring_active = false;
|
||||
reset_log_snapshot();
|
||||
return;
|
||||
}
|
||||
|
||||
size_t skip = 0;
|
||||
if (g_mdbg.log_snapshot_length != 0 && text_len >= g_mdbg.log_snapshot_length &&
|
||||
!memcmp(g_mdbg.log_snapshot, text, g_mdbg.log_snapshot_length)) {
|
||||
skip = g_mdbg.log_snapshot_length;
|
||||
} else {
|
||||
skip = find_log_overlap(text, text_len);
|
||||
if (skip == 0 && g_mdbg.log_snapshot_length != 0) {
|
||||
log_debug(" [MDBG] log stream reset or truncated for %s",
|
||||
g_mdbg.game.title_id);
|
||||
reset_log_line_buffer();
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = skip; i < text_len; ++i) {
|
||||
append_log_char(text[i], now_us);
|
||||
if (!g_mdbg.game.active)
|
||||
return;
|
||||
}
|
||||
|
||||
update_log_snapshot(text, text_len);
|
||||
}
|
||||
|
||||
static void start_log_monitoring(void) {
|
||||
if (!g_mdbg.game.active)
|
||||
return;
|
||||
|
||||
if (!ensure_mdbg_privileges()) {
|
||||
g_mdbg.game.log_monitoring_active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ensure_log_buffers()) {
|
||||
g_mdbg.game.log_monitoring_active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
g_mdbg.game.log_monitoring_active = true;
|
||||
|
||||
const char *text = NULL;
|
||||
size_t text_len = 0;
|
||||
int ret = fetch_log_text(&text, &text_len);
|
||||
if (ret < 0) {
|
||||
log_debug(" [MDBG] initial log snapshot failed for %s: 0x%08x",
|
||||
g_mdbg.game.title_id, ret);
|
||||
g_mdbg.game.log_monitoring_active = false;
|
||||
reset_log_snapshot();
|
||||
return;
|
||||
}
|
||||
|
||||
update_log_snapshot(text, text_len);
|
||||
reset_log_line_buffer();
|
||||
}
|
||||
|
||||
static void handle_crash_candidate(uint64_t flags, uint64_t now_us) {
|
||||
if (!g_mdbg.game.active)
|
||||
return;
|
||||
|
||||
log_debug(" [MDBG] crash-candidate: %s pid=%ld flags=0x%08" PRIx64,
|
||||
g_mdbg.game.title_id, (long)g_mdbg.game.pid, flags);
|
||||
|
||||
handle_post_pause_failure("crash-candidate", now_us);
|
||||
}
|
||||
|
||||
void sm_mdbg_init(void) {
|
||||
memset(&g_mdbg, 0, sizeof(g_mdbg));
|
||||
}
|
||||
|
||||
void sm_mdbg_shutdown(void) {
|
||||
free_log_buffers();
|
||||
memset(&g_mdbg, 0, sizeof(g_mdbg));
|
||||
}
|
||||
|
||||
void sm_mdbg_game_on_exec(pid_t pid, const char *title_id, uint32_t app_id) {
|
||||
if (pid <= 0 || !title_id || title_id[0] == '\0')
|
||||
return;
|
||||
if (!sm_mdbg_enabled()) {
|
||||
clear_tracked_game();
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_mdbg.game.active && g_mdbg.game.pid != pid) {
|
||||
log_debug(" [MDBG] replacing tracked game pid=%ld (%s) with pid=%ld (%s)",
|
||||
(long)g_mdbg.game.pid, g_mdbg.game.title_id, (long)pid, title_id);
|
||||
}
|
||||
|
||||
clear_tracked_game();
|
||||
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
g_mdbg.game.active = true;
|
||||
g_mdbg.game.pid = pid;
|
||||
g_mdbg.game.next_poll_us = now_us;
|
||||
(void)strlcpy(g_mdbg.game.title_id, title_id, sizeof(g_mdbg.game.title_id));
|
||||
int rtld_prefix_len =
|
||||
snprintf(g_mdbg.game.rtld_error_prefix,
|
||||
sizeof(g_mdbg.game.rtld_error_prefix),
|
||||
"[rtld] <%ld> ERROR", (long)pid);
|
||||
if (rtld_prefix_len <= 0 ||
|
||||
(size_t)rtld_prefix_len >= sizeof(g_mdbg.game.rtld_error_prefix)) {
|
||||
g_mdbg.game.rtld_error_prefix[0] = '\0';
|
||||
}
|
||||
|
||||
log_debug(" [MDBG] tracking crash-candidate state: %s pid=%ld app_id=0x%08X",
|
||||
g_mdbg.game.title_id, (long)pid, app_id);
|
||||
}
|
||||
|
||||
void sm_mdbg_game_on_kstuff_pause(pid_t pid, uint64_t pause_time_us,
|
||||
uint32_t pause_delay_seconds) {
|
||||
if (!g_mdbg.game.active || g_mdbg.game.pid != pid)
|
||||
return;
|
||||
|
||||
g_mdbg.game.pause_seen = true;
|
||||
g_mdbg.game.pause_time_us = pause_time_us;
|
||||
g_mdbg.game.monitor_deadline_us =
|
||||
g_mdbg.game.pause_time_us + MDBG_AUTOTUNE_WINDOW_US;
|
||||
g_mdbg.game.pause_delay_seconds = pause_delay_seconds;
|
||||
g_mdbg.game.next_poll_us = pause_time_us;
|
||||
start_log_monitoring();
|
||||
}
|
||||
|
||||
void sm_mdbg_game_on_exit(pid_t pid) {
|
||||
if (!g_mdbg.game.active || g_mdbg.game.pid != pid)
|
||||
return;
|
||||
|
||||
clear_tracked_game();
|
||||
}
|
||||
|
||||
void sm_mdbg_game_shutdown(void) {
|
||||
clear_tracked_game();
|
||||
}
|
||||
|
||||
uint64_t sm_mdbg_next_wake_us(uint64_t now_us) {
|
||||
(void)now_us;
|
||||
if (!sm_mdbg_enabled())
|
||||
return 0;
|
||||
if (!g_mdbg.game.active)
|
||||
return 0;
|
||||
|
||||
uint64_t next_wake_us = g_mdbg.game.next_poll_us;
|
||||
if (g_mdbg.game.monitor_deadline_us != 0 &&
|
||||
(next_wake_us == 0 || g_mdbg.game.monitor_deadline_us < next_wake_us)) {
|
||||
next_wake_us = g_mdbg.game.monitor_deadline_us;
|
||||
}
|
||||
return next_wake_us;
|
||||
}
|
||||
|
||||
void sm_mdbg_poll(void) {
|
||||
if (!sm_mdbg_enabled())
|
||||
return;
|
||||
if (!g_mdbg.game.active)
|
||||
return;
|
||||
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
if (now_us < g_mdbg.game.next_poll_us) {
|
||||
return;
|
||||
}
|
||||
if (g_mdbg.game.pause_seen && g_mdbg.game.monitor_deadline_us != 0 &&
|
||||
now_us >= g_mdbg.game.monitor_deadline_us) {
|
||||
log_debug(" [MDBG] crash monitoring window expired for %s pid=%ld",
|
||||
g_mdbg.game.title_id, (long)g_mdbg.game.pid);
|
||||
clear_tracked_game();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ensure_mdbg_privileges()) {
|
||||
g_mdbg.game.next_poll_us = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
poll_log_monitor(now_us);
|
||||
if (!g_mdbg.game.active)
|
||||
return;
|
||||
|
||||
g_mdbg.game.next_poll_us = now_us + GAME_LIFECYCLE_POLL_INTERVAL_US;
|
||||
|
||||
uint64_t flags = 0;
|
||||
int ret = query_mdbg_flags(g_mdbg.game.pid, &flags);
|
||||
if (is_mdbg_process_gone_error(ret)) {
|
||||
clear_tracked_game();
|
||||
return;
|
||||
}
|
||||
if (ret != 0)
|
||||
return;
|
||||
|
||||
if ((flags & MDBG_FLAG_EXCEPTION_STOP) == 0)
|
||||
return;
|
||||
|
||||
handle_crash_candidate(flags, now_us);
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
#include "sm_platform.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_mount_device.h"
|
||||
#include "sm_image_cache.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_mount_defs.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_stability.h"
|
||||
|
||||
const char *attach_backend_name(attach_backend_t backend) {
|
||||
if (backend == ATTACH_BACKEND_LVD)
|
||||
return "LVD";
|
||||
if (backend == ATTACH_BACKEND_MD)
|
||||
return "MD";
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
// --- Device Node Wait and Source Stability ---
|
||||
bool wait_for_dev_node_state(const char *devname, bool should_exist) {
|
||||
for (int i = 0; i < LVD_NODE_WAIT_RETRIES; i++) {
|
||||
if (path_exists(devname) == should_exist)
|
||||
return true;
|
||||
sceKernelUsleep(LVD_NODE_WAIT_US);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool is_source_stable_for_mount(const char *path, const char *name,
|
||||
const char *tag) {
|
||||
double age = 0.0;
|
||||
int st_err = 0;
|
||||
if (is_path_stable_now(path, &age, &st_err))
|
||||
return true;
|
||||
if (st_err != 0)
|
||||
return false;
|
||||
log_debug(" [%s] %s modified %.0fs ago, waiting...", tag, name, age);
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Mounted Device Resolution (/dev/lvdN, /dev/mdN) ---
|
||||
static bool parse_unit_from_dev_path(const char *dev_path, const char *prefix,
|
||||
int *unit_out) {
|
||||
size_t prefix_len = strlen(prefix);
|
||||
if (strncmp(dev_path, prefix, prefix_len) != 0)
|
||||
return false;
|
||||
|
||||
char *end = NULL;
|
||||
long unit = strtol(dev_path + prefix_len, &end, 10);
|
||||
if (end == dev_path + prefix_len || *end != '\0' || unit < 0 ||
|
||||
unit > INT_MAX)
|
||||
return false;
|
||||
|
||||
*unit_out = (int)unit;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool resolve_device_from_mount(const char *mount_point,
|
||||
attach_backend_t *backend_out, int *unit_out) {
|
||||
*backend_out = ATTACH_BACKEND_NONE;
|
||||
*unit_out = -1;
|
||||
|
||||
if (resolve_device_from_mount_cache(mount_point, backend_out, unit_out))
|
||||
return true;
|
||||
|
||||
struct statfs sfs;
|
||||
if (statfs(mount_point, &sfs) != 0)
|
||||
return false;
|
||||
|
||||
if (strcmp(sfs.f_mntonname, mount_point) != 0)
|
||||
return false;
|
||||
|
||||
if (parse_unit_from_dev_path(sfs.f_mntfromname, "/dev/lvd", unit_out)) {
|
||||
*backend_out = ATTACH_BACKEND_LVD;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (parse_unit_from_dev_path(sfs.f_mntfromname, "/dev/md", unit_out)) {
|
||||
*backend_out = ATTACH_BACKEND_MD;
|
||||
return true;
|
||||
}
|
||||
|
||||
struct statfs *mntbuf = NULL;
|
||||
int mntcount = getmntinfo(&mntbuf, MNT_NOWAIT);
|
||||
if (mntcount <= 0 || !mntbuf)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < mntcount; i++) {
|
||||
if (strcmp(mntbuf[i].f_mntonname, mount_point) != 0)
|
||||
continue;
|
||||
if (parse_unit_from_dev_path(mntbuf[i].f_mntfromname, "/dev/lvd", unit_out)) {
|
||||
*backend_out = ATTACH_BACKEND_LVD;
|
||||
return true;
|
||||
}
|
||||
if (parse_unit_from_dev_path(mntbuf[i].f_mntfromname, "/dev/md", unit_out)) {
|
||||
*backend_out = ATTACH_BACKEND_MD;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool is_path_mountpoint(const char *path) {
|
||||
struct statfs sfs;
|
||||
return (statfs(path, &sfs) == 0 && strcmp(sfs.f_mntonname, path) == 0);
|
||||
}
|
||||
|
||||
bool is_active_image_mount_point(const char *path) {
|
||||
return is_path_mountpoint(path);
|
||||
}
|
||||
|
||||
bool wait_for_lvd_release(void) {
|
||||
for (unsigned int waited_us = 0;; waited_us += LVD_RELEASE_WAIT_POLL_US) {
|
||||
struct statfs *mntbuf = NULL;
|
||||
int mntcount = getmntinfo(&mntbuf, MNT_NOWAIT);
|
||||
bool mounted = false;
|
||||
for (int i = 0; i < mntcount && mntbuf; i++) {
|
||||
if (strcmp(mntbuf[i].f_mntfromname, "/dev/lvd2") != 0)
|
||||
continue;
|
||||
mounted = true;
|
||||
break;
|
||||
}
|
||||
if (!mounted) {
|
||||
if (waited_us != 0)
|
||||
log_debug(" [IMG][LVD] /dev/lvd2 released");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (waited_us == 0) {
|
||||
log_debug(" [IMG][LVD] waiting for /dev/lvd2 to be released...");
|
||||
for (int i = 0; i < mntcount && mntbuf; i++) {
|
||||
if (strncmp(mntbuf[i].f_mntfromname, "/dev/lvd", 8) != 0)
|
||||
continue;
|
||||
log_debug(" [IMG][LVD] mounted: from=%s path=%s type=%s "
|
||||
"bsize=%llu iosize=%llu blocks=%llu bfree=%llu "
|
||||
"bavail=%llu files=%llu ffree=%llu flags=0x%lX",
|
||||
mntbuf[i].f_mntfromname, mntbuf[i].f_mntonname,
|
||||
mntbuf[i].f_fstypename,
|
||||
(unsigned long long)(uint64_t)mntbuf[i].f_bsize,
|
||||
(unsigned long long)(uint64_t)mntbuf[i].f_iosize,
|
||||
(unsigned long long)(uint64_t)mntbuf[i].f_blocks,
|
||||
(unsigned long long)(uint64_t)mntbuf[i].f_bfree,
|
||||
(unsigned long long)(uint64_t)mntbuf[i].f_bavail,
|
||||
(unsigned long long)(uint64_t)mntbuf[i].f_files,
|
||||
(unsigned long long)(uint64_t)mntbuf[i].f_ffree,
|
||||
(unsigned long)mntbuf[i].f_flags);
|
||||
}
|
||||
}
|
||||
if (should_stop_requested())
|
||||
return false;
|
||||
if (waited_us >= LVD_RELEASE_WAIT_MAX_US) {
|
||||
log_debug(" [IMG][LVD] /dev/lvd2 wait timeout reached (%u ms), "
|
||||
"continuing startup", LVD_RELEASE_WAIT_MAX_US / 1000u);
|
||||
return true;
|
||||
}
|
||||
sceKernelUsleep(LVD_RELEASE_WAIT_POLL_US);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Device Detach Helpers ---
|
||||
static bool detach_lvd_unit(int unit_id) {
|
||||
if (unit_id < 0)
|
||||
return true;
|
||||
|
||||
int fd = open(LVD_CTRL_PATH, O_RDWR);
|
||||
if (fd < 0) {
|
||||
log_debug(" [IMG][%s] open %s for detach failed: %s",
|
||||
attach_backend_name(ATTACH_BACKEND_LVD), LVD_CTRL_PATH,
|
||||
strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
lvd_ioctl_detach_t req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.device_id = unit_id;
|
||||
|
||||
bool ok = true;
|
||||
if (ioctl(fd, SCE_LVD_IOC_DETACH, &req) != 0) {
|
||||
log_debug(" [IMG][%s] detach %d failed: %s",
|
||||
attach_backend_name(ATTACH_BACKEND_LVD), unit_id, strerror(errno));
|
||||
ok = false;
|
||||
}
|
||||
close(fd);
|
||||
|
||||
char devname[64];
|
||||
snprintf(devname, sizeof(devname), "/dev/lvd%d", unit_id);
|
||||
if (!wait_for_dev_node_state(devname, false)) {
|
||||
log_debug(" [IMG][%s] device node still present after detach: /dev/lvd%d",
|
||||
attach_backend_name(ATTACH_BACKEND_LVD), unit_id);
|
||||
ok = false;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
static bool detach_md_unit(int unit_id) {
|
||||
if (unit_id < 0)
|
||||
return true;
|
||||
|
||||
int fd = open(MD_CTRL_PATH, O_RDWR);
|
||||
if (fd < 0) {
|
||||
log_debug(" [IMG][%s] open %s for detach failed: %s",
|
||||
attach_backend_name(ATTACH_BACKEND_MD), MD_CTRL_PATH,
|
||||
strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
struct md_ioctl req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.md_version = MDIOVERSION;
|
||||
req.md_unit = (unsigned int)unit_id;
|
||||
|
||||
bool ok = true;
|
||||
if (ioctl(fd, MDIOCDETACH, &req) != 0) {
|
||||
int err = errno;
|
||||
req.md_options = MD_FORCE;
|
||||
if (ioctl(fd, MDIOCDETACH, &req) != 0) {
|
||||
log_debug(" [IMG][%s] detach %d failed: %s",
|
||||
attach_backend_name(ATTACH_BACKEND_MD), unit_id,
|
||||
strerror(errno));
|
||||
ok = false;
|
||||
} else {
|
||||
log_debug(" [IMG][%s] detach %d forced after error: %s",
|
||||
attach_backend_name(ATTACH_BACKEND_MD), unit_id,
|
||||
strerror(err));
|
||||
}
|
||||
}
|
||||
close(fd);
|
||||
|
||||
char devname[64];
|
||||
snprintf(devname, sizeof(devname), "/dev/md%d", unit_id);
|
||||
if (!wait_for_dev_node_state(devname, false)) {
|
||||
log_debug(" [IMG][%s] device node still present after detach: /dev/md%d",
|
||||
attach_backend_name(ATTACH_BACKEND_MD), unit_id);
|
||||
ok = false;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool detach_attached_unit(attach_backend_t backend, int unit_id) {
|
||||
if (backend == ATTACH_BACKEND_MD)
|
||||
return detach_md_unit(unit_id);
|
||||
else if (backend == ATTACH_BACKEND_LVD)
|
||||
return detach_lvd_unit(unit_id);
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
#include "sm_platform.h"
|
||||
#include "sm_path_state.h"
|
||||
#include "sm_filesystem.h"
|
||||
#include "sm_hash.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_path_utils.h"
|
||||
|
||||
struct PathStateEntry {
|
||||
char path[MAX_PATH];
|
||||
uint8_t missing_param_attempts;
|
||||
uint8_t image_mount_attempts;
|
||||
bool missing_param_limit_logged;
|
||||
bool image_mount_limit_logged;
|
||||
bool backport_mount_blocked;
|
||||
bool manual_missing_source_logged;
|
||||
bool game_info_cached;
|
||||
bool game_info_valid;
|
||||
time_t game_info_mtime;
|
||||
off_t game_info_size;
|
||||
ino_t game_info_ino;
|
||||
char game_title_id[MAX_TITLE_ID];
|
||||
char game_title_name[MAX_TITLE_NAME];
|
||||
bool valid;
|
||||
};
|
||||
|
||||
static struct PathStateEntry g_path_state[PATH_STATE_CAPACITY];
|
||||
static uint16_t g_path_state_hash[STATE_HASH_SIZE];
|
||||
|
||||
static void rebuild_path_state_hash(void) {
|
||||
memset(g_path_state_hash, 0, sizeof(g_path_state_hash));
|
||||
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
|
||||
if (!g_path_state[k].valid || g_path_state[k].path[0] == '\0')
|
||||
continue;
|
||||
uint32_t slot = sm_fnv1a32(g_path_state[k].path) & (STATE_HASH_SIZE - 1u);
|
||||
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
|
||||
if (g_path_state_hash[slot] == 0) {
|
||||
g_path_state_hash[slot] = (uint16_t)(k + 1);
|
||||
break;
|
||||
}
|
||||
slot = (slot + 1u) & (STATE_HASH_SIZE - 1u);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static struct PathStateEntry *find_path_state(const char *path) {
|
||||
uint32_t slot = sm_fnv1a32(path) & (STATE_HASH_SIZE - 1u);
|
||||
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
|
||||
uint16_t idx = g_path_state_hash[slot];
|
||||
if (idx == 0)
|
||||
return NULL;
|
||||
struct PathStateEntry *entry = &g_path_state[idx - 1u];
|
||||
if (entry->valid && strcmp(entry->path, path) == 0)
|
||||
return entry;
|
||||
slot = (slot + 1u) & (STATE_HASH_SIZE - 1u);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static struct PathStateEntry *create_path_state(const char *path) {
|
||||
int slot_k = -1;
|
||||
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
|
||||
if (!g_path_state[k].valid) {
|
||||
slot_k = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (slot_k < 0) {
|
||||
int evict_k = -1;
|
||||
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
|
||||
if (!g_path_state[k].valid)
|
||||
continue;
|
||||
if (!path_exists(g_path_state[k].path) &&
|
||||
!g_path_state[k].manual_missing_source_logged) {
|
||||
evict_k = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (evict_k < 0) {
|
||||
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
|
||||
if (!g_path_state[k].valid)
|
||||
continue;
|
||||
if (g_path_state[k].missing_param_attempts == 0 &&
|
||||
g_path_state[k].image_mount_attempts == 0 &&
|
||||
!g_path_state[k].backport_mount_blocked &&
|
||||
!g_path_state[k].manual_missing_source_logged &&
|
||||
!g_path_state[k].game_info_cached) {
|
||||
evict_k = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (evict_k < 0)
|
||||
evict_k = 0;
|
||||
memset(&g_path_state[evict_k], 0, sizeof(g_path_state[evict_k]));
|
||||
rebuild_path_state_hash();
|
||||
slot_k = evict_k;
|
||||
}
|
||||
|
||||
memset(&g_path_state[slot_k], 0, sizeof(g_path_state[slot_k]));
|
||||
g_path_state[slot_k].valid = true;
|
||||
(void)strlcpy(g_path_state[slot_k].path, path,
|
||||
sizeof(g_path_state[slot_k].path));
|
||||
|
||||
uint32_t slot = sm_fnv1a32(path) & (STATE_HASH_SIZE - 1u);
|
||||
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
|
||||
if (g_path_state_hash[slot] == 0) {
|
||||
g_path_state_hash[slot] = (uint16_t)(slot_k + 1);
|
||||
return &g_path_state[slot_k];
|
||||
}
|
||||
slot = (slot + 1u) & (STATE_HASH_SIZE - 1u);
|
||||
}
|
||||
|
||||
g_path_state[slot_k].valid = false;
|
||||
g_path_state[slot_k].path[0] = '\0';
|
||||
rebuild_path_state_hash();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static struct PathStateEntry *get_or_create_path_state(const char *path) {
|
||||
struct PathStateEntry *entry = find_path_state(path);
|
||||
return entry ? entry : create_path_state(path);
|
||||
}
|
||||
|
||||
bool load_cached_game_info(const char *path, const struct stat *param_st,
|
||||
char *out_id, char *out_name, bool *valid_out) {
|
||||
*valid_out = false;
|
||||
|
||||
struct PathStateEntry *entry = find_path_state(path);
|
||||
if (!entry || !entry->game_info_cached ||
|
||||
entry->game_info_mtime != param_st->st_mtime ||
|
||||
entry->game_info_size != param_st->st_size ||
|
||||
entry->game_info_ino != param_st->st_ino) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*valid_out = entry->game_info_valid;
|
||||
if (!entry->game_info_valid)
|
||||
return true;
|
||||
|
||||
(void)strlcpy(out_id, entry->game_title_id, MAX_TITLE_ID);
|
||||
(void)strlcpy(out_name, entry->game_title_name, MAX_TITLE_NAME);
|
||||
return true;
|
||||
}
|
||||
|
||||
void store_cached_game_info(const char *path, const struct stat *param_st,
|
||||
bool valid, const char *title_id,
|
||||
const char *title_name) {
|
||||
struct PathStateEntry *entry = get_or_create_path_state(path);
|
||||
entry->game_info_cached = true;
|
||||
entry->game_info_valid = valid;
|
||||
entry->game_info_mtime = param_st->st_mtime;
|
||||
entry->game_info_size = param_st->st_size;
|
||||
entry->game_info_ino = param_st->st_ino;
|
||||
(void)strlcpy(entry->game_title_id, valid ? title_id : "", MAX_TITLE_ID);
|
||||
(void)strlcpy(entry->game_title_name, valid ? title_name : "", MAX_TITLE_NAME);
|
||||
}
|
||||
|
||||
void prune_path_state(void) {
|
||||
bool changed = false;
|
||||
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
|
||||
if (!g_path_state[k].valid || g_path_state[k].path[0] == '\0')
|
||||
continue;
|
||||
if (path_exists(g_path_state[k].path))
|
||||
continue;
|
||||
if (g_path_state[k].manual_missing_source_logged)
|
||||
continue;
|
||||
memset(&g_path_state[k], 0, sizeof(g_path_state[k]));
|
||||
changed = true;
|
||||
}
|
||||
if (changed)
|
||||
rebuild_path_state_hash();
|
||||
}
|
||||
|
||||
void prune_path_state_for_root(const char *root) {
|
||||
if (!root || root[0] == '\0') {
|
||||
prune_path_state();
|
||||
return;
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
|
||||
if (!g_path_state[k].valid || g_path_state[k].path[0] == '\0')
|
||||
continue;
|
||||
if (!path_matches_root_or_child(g_path_state[k].path, root))
|
||||
continue;
|
||||
if (path_exists(g_path_state[k].path))
|
||||
continue;
|
||||
if (g_path_state[k].manual_missing_source_logged)
|
||||
continue;
|
||||
memset(&g_path_state[k], 0, sizeof(g_path_state[k]));
|
||||
changed = true;
|
||||
}
|
||||
if (changed)
|
||||
rebuild_path_state_hash();
|
||||
}
|
||||
|
||||
bool is_missing_param_scan_limited(const char *path) {
|
||||
if (!is_under_image_mount_base(path))
|
||||
return false;
|
||||
struct PathStateEntry *entry = find_path_state(path);
|
||||
if (!entry)
|
||||
return false;
|
||||
return entry->missing_param_attempts >= MAX_MISSING_PARAM_SCAN_ATTEMPTS;
|
||||
}
|
||||
|
||||
void record_missing_param_failure(const char *path) {
|
||||
if (!is_under_image_mount_base(path))
|
||||
return;
|
||||
|
||||
struct PathStateEntry *entry = get_or_create_path_state(path);
|
||||
if (entry->missing_param_attempts < UINT8_MAX)
|
||||
entry->missing_param_attempts++;
|
||||
|
||||
log_debug(" [SCAN] missing/invalid param.json: %s", path);
|
||||
if (entry->missing_param_attempts == 1)
|
||||
notify_system("Missing/invalid param.json:\n%s", path);
|
||||
if (entry->missing_param_attempts >= MAX_MISSING_PARAM_SCAN_ATTEMPTS &&
|
||||
!entry->missing_param_limit_logged) {
|
||||
log_debug(" [SCAN] attempt limit reached (%u), skipping path: %s",
|
||||
(unsigned)MAX_MISSING_PARAM_SCAN_ATTEMPTS, path);
|
||||
entry->missing_param_limit_logged = true;
|
||||
}
|
||||
}
|
||||
|
||||
void clear_missing_param_entry(const char *path) {
|
||||
struct PathStateEntry *entry = find_path_state(path);
|
||||
if (!entry)
|
||||
return;
|
||||
entry->missing_param_attempts = 0;
|
||||
entry->missing_param_limit_logged = false;
|
||||
}
|
||||
|
||||
bool is_image_mount_limited(const char *path) {
|
||||
struct PathStateEntry *entry = find_path_state(path);
|
||||
return entry ? entry->image_mount_attempts >= MAX_IMAGE_MOUNT_ATTEMPTS
|
||||
: false;
|
||||
}
|
||||
|
||||
uint8_t bump_image_mount_attempts(const char *path) {
|
||||
struct PathStateEntry *entry = get_or_create_path_state(path);
|
||||
if (entry->image_mount_attempts < UINT8_MAX)
|
||||
entry->image_mount_attempts++;
|
||||
if (entry->image_mount_attempts >= MAX_IMAGE_MOUNT_ATTEMPTS &&
|
||||
!entry->image_mount_limit_logged) {
|
||||
log_debug(" [IMG] retry limit reached (%u/%u), skipping image: %s",
|
||||
(unsigned)entry->image_mount_attempts,
|
||||
(unsigned)MAX_IMAGE_MOUNT_ATTEMPTS, path);
|
||||
entry->image_mount_limit_logged = true;
|
||||
}
|
||||
return entry->image_mount_attempts;
|
||||
}
|
||||
|
||||
void clear_image_mount_attempts(const char *path) {
|
||||
struct PathStateEntry *entry = find_path_state(path);
|
||||
if (!entry)
|
||||
return;
|
||||
entry->image_mount_attempts = 0;
|
||||
entry->image_mount_limit_logged = false;
|
||||
}
|
||||
|
||||
bool is_backport_mount_blocked(const char *path) {
|
||||
if (!path || path[0] == '\0')
|
||||
return false;
|
||||
struct PathStateEntry *entry = find_path_state(path);
|
||||
return entry ? entry->backport_mount_blocked : false;
|
||||
}
|
||||
|
||||
void note_backport_mount_failure(const char *path) {
|
||||
if (!path || path[0] == '\0')
|
||||
return;
|
||||
struct PathStateEntry *entry = get_or_create_path_state(path);
|
||||
if (!entry)
|
||||
return;
|
||||
if (entry->backport_mount_blocked)
|
||||
return;
|
||||
entry->backport_mount_blocked = true;
|
||||
}
|
||||
|
||||
void clear_backport_mount_failure(const char *path) {
|
||||
if (!path || path[0] == '\0')
|
||||
return;
|
||||
struct PathStateEntry *entry = find_path_state(path);
|
||||
if (!entry)
|
||||
return;
|
||||
entry->backport_mount_blocked = false;
|
||||
}
|
||||
|
||||
bool note_manual_missing_source_once(const char *path) {
|
||||
if (!path || path[0] == '\0')
|
||||
return false;
|
||||
struct PathStateEntry *entry = get_or_create_path_state(path);
|
||||
if (!entry || entry->manual_missing_source_logged)
|
||||
return false;
|
||||
entry->manual_missing_source_logged = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void clear_manual_missing_source(const char *path) {
|
||||
if (!path || path[0] == '\0')
|
||||
return;
|
||||
struct PathStateEntry *entry = find_path_state(path);
|
||||
if (!entry)
|
||||
return;
|
||||
entry->manual_missing_source_logged = false;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#include "sm_platform.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_paths.h"
|
||||
|
||||
const char *get_filename_component(const char *path) {
|
||||
const char *base = strrchr(path, '/');
|
||||
if (!base)
|
||||
base = strrchr(path, '\\');
|
||||
return base ? base + 1 : path;
|
||||
}
|
||||
|
||||
bool path_exists(const char *path) {
|
||||
struct stat st;
|
||||
return (path && path[0] != '\0' && stat(path, &st) == 0);
|
||||
}
|
||||
|
||||
bool is_under_image_mount_base(const char *path) {
|
||||
size_t image_prefix_len = strlen(IMAGE_MOUNT_BASE);
|
||||
return (strncmp(path, IMAGE_MOUNT_BASE, image_prefix_len) == 0 &&
|
||||
path[image_prefix_len] == '/');
|
||||
}
|
||||
|
||||
bool is_pfsc_image_mount_base_or_child(const char *path) {
|
||||
return path_matches_root_or_child(path, PFSC_IMAGE_MOUNT_BASE);
|
||||
}
|
||||
|
||||
bool path_matches_root_or_child(const char *path, const char *root) {
|
||||
if (!path || !root || root[0] == '\0')
|
||||
return false;
|
||||
size_t root_len = strlen(root);
|
||||
if (strncmp(path, root, root_len) != 0)
|
||||
return false;
|
||||
return path[root_len] == '\0' || path[root_len] == '/';
|
||||
}
|
||||
|
||||
bool is_usb_storage_path(const char *path) {
|
||||
static const char *usb_roots[] = {
|
||||
"/mnt/usb0", "/mnt/usb1", "/mnt/usb2", "/mnt/usb3",
|
||||
"/mnt/usb4", "/mnt/usb5", "/mnt/usb6", "/mnt/usb7",
|
||||
"/mnt/ext0",
|
||||
};
|
||||
|
||||
if (!path || path[0] == '\0')
|
||||
return false;
|
||||
|
||||
for (size_t i = 0; i < sizeof(usb_roots) / sizeof(usb_roots[0]); i++) {
|
||||
if (path_matches_root_or_child(path, usb_roots[i]))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool build_backports_root_path(const char *scan_path, char out[MAX_PATH]) {
|
||||
if (is_under_image_mount_base(scan_path))
|
||||
return false;
|
||||
|
||||
int written = snprintf(out, MAX_PATH, "%s/%s", scan_path,
|
||||
DEFAULT_BACKPORTS_DIR_NAME);
|
||||
return written >= 0 && (size_t)written < MAX_PATH;
|
||||
}
|
||||
+903
@@ -0,0 +1,903 @@
|
||||
#include "sm_platform.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_scan.h"
|
||||
#include "sm_scan_tree.h"
|
||||
#include "sm_types.h"
|
||||
#include "sm_game_cache.h"
|
||||
#include "sm_gameinfo.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_mount_device.h"
|
||||
#include "sm_filesystem.h"
|
||||
#include "sm_appdb.h"
|
||||
#include "sm_paths.h"
|
||||
#include "sm_path_state.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_stability.h"
|
||||
#include "sm_title_state.h"
|
||||
#include "sm_image_cache.h"
|
||||
#include "sm_image.h"
|
||||
#include "sm_install_queue.h"
|
||||
#include "sm_manual.h"
|
||||
|
||||
typedef struct {
|
||||
char discovered_param_roots[MAX_PENDING][MAX_PATH];
|
||||
char checked_appmeta_titles[MAX_PENDING][MAX_TITLE_ID];
|
||||
char blocked_ppsa_uninstall_titles[MAX_PENDING][MAX_TITLE_ID];
|
||||
bool checked_appmeta_present[MAX_PENDING];
|
||||
int checked_appmeta_count;
|
||||
int blocked_ppsa_uninstall_count;
|
||||
} scan_workspace_t;
|
||||
|
||||
// Reuse the largest transient scan buffer instead of placing ~512 KiB of path
|
||||
// state on the stack each cycle.
|
||||
static scan_workspace_t g_scan_workspace;
|
||||
|
||||
static void reset_scan_workspace(void) {
|
||||
g_scan_workspace.checked_appmeta_count = 0;
|
||||
g_scan_workspace.blocked_ppsa_uninstall_count = 0;
|
||||
}
|
||||
|
||||
static bool blocked_ppsa_uninstall_requested(const char *title_id) {
|
||||
if (!title_id || title_id[0] == '\0')
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < g_scan_workspace.blocked_ppsa_uninstall_count; i++) {
|
||||
if (strcmp(g_scan_workspace.blocked_ppsa_uninstall_titles[i], title_id) == 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void remember_blocked_ppsa_uninstall(const char *title_id) {
|
||||
if (!title_id || title_id[0] == '\0' ||
|
||||
blocked_ppsa_uninstall_requested(title_id))
|
||||
return;
|
||||
|
||||
if (g_scan_workspace.blocked_ppsa_uninstall_count >= MAX_PENDING)
|
||||
return;
|
||||
|
||||
int slot = g_scan_workspace.blocked_ppsa_uninstall_count++;
|
||||
(void)strlcpy(g_scan_workspace.blocked_ppsa_uninstall_titles[slot], title_id,
|
||||
sizeof(g_scan_workspace.blocked_ppsa_uninstall_titles[slot]));
|
||||
}
|
||||
|
||||
static bool is_blocked_ppsa_title(
|
||||
const struct AppDbTitleList *blocked_ppsa_titles,
|
||||
bool blocked_ppsa_titles_ready, const char *title_id) {
|
||||
return blocked_ppsa_titles_ready &&
|
||||
app_db_title_list_contains(blocked_ppsa_titles, title_id);
|
||||
}
|
||||
|
||||
static bool request_blocked_ppsa_uninstall(const char *title_id,
|
||||
const char *source_path) {
|
||||
if (!title_id || title_id[0] == '\0' ||
|
||||
blocked_ppsa_uninstall_requested(title_id))
|
||||
return false;
|
||||
|
||||
remember_blocked_ppsa_uninstall(title_id);
|
||||
int res = sceAppInstUtilAppUnInstall(title_id);
|
||||
if (res == 0) {
|
||||
sm_install_forget_pending_title(title_id);
|
||||
invalidate_app_db_title_cache();
|
||||
if (source_path && source_path[0] != '\0') {
|
||||
log_debug(" [REG] requested blocked PPSA uninstall: %s source=%s",
|
||||
title_id, source_path);
|
||||
} else {
|
||||
log_debug(" [REG] requested blocked PPSA uninstall: %s", title_id);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
log_debug(" [REG] blocked PPSA uninstall failed: %s code=0x%08X", title_id,
|
||||
(uint32_t)res);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool get_appmeta_present_for_scan_cycle(const char *title_id) {
|
||||
for (int i = 0; i < g_scan_workspace.checked_appmeta_count; i++) {
|
||||
if (strcmp(g_scan_workspace.checked_appmeta_titles[i], title_id) == 0)
|
||||
return g_scan_workspace.checked_appmeta_present[i];
|
||||
}
|
||||
|
||||
bool present = has_appmeta_data(title_id);
|
||||
if (g_scan_workspace.checked_appmeta_count < MAX_PENDING) {
|
||||
int slot = g_scan_workspace.checked_appmeta_count++;
|
||||
(void)strlcpy(g_scan_workspace.checked_appmeta_titles[slot], title_id,
|
||||
sizeof(g_scan_workspace.checked_appmeta_titles[slot]));
|
||||
g_scan_workspace.checked_appmeta_present[slot] = present;
|
||||
}
|
||||
return present;
|
||||
}
|
||||
|
||||
static bool is_under_discovered_param_root(
|
||||
const char *path, char discovered_param_roots[][MAX_PATH],
|
||||
int discovered_count) {
|
||||
for (int i = 0; i < discovered_count; i++) {
|
||||
const char *root = discovered_param_roots[i];
|
||||
size_t root_len = strlen(root);
|
||||
if (strncmp(path, root, root_len) != 0)
|
||||
continue;
|
||||
if (path[root_len] == '\0' || path[root_len] == '/')
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Candidate Discovery ---
|
||||
typedef struct {
|
||||
char title_id[MAX_TITLE_ID];
|
||||
char title_name[MAX_TITLE_NAME];
|
||||
} directory_candidate_info_t;
|
||||
|
||||
typedef enum {
|
||||
DIRECTORY_CANDIDATE_DESCEND = 0,
|
||||
DIRECTORY_CANDIDATE_SKIP_DESCEND,
|
||||
DIRECTORY_CANDIDATE_READY,
|
||||
} directory_candidate_probe_t;
|
||||
|
||||
typedef enum {
|
||||
EXISTING_DIRECTORY_CONTINUE = 0,
|
||||
EXISTING_DIRECTORY_HANDLED,
|
||||
EXISTING_DIRECTORY_PREFER_CURRENT,
|
||||
EXISTING_DIRECTORY_PREFER_CACHED,
|
||||
} existing_directory_result_t;
|
||||
|
||||
typedef struct {
|
||||
const struct AppDbTitleList *titles;
|
||||
const struct AppDbTitleList *blocked_ppsa_titles;
|
||||
bool titles_ready;
|
||||
bool blocked_ppsa_titles_ready;
|
||||
} scan_app_db_context_t;
|
||||
|
||||
static directory_candidate_probe_t probe_directory_candidate(
|
||||
const char *full_path, char discovered_param_roots[][MAX_PATH],
|
||||
int *discovered_param_root_count, bool allow_known_param_root,
|
||||
directory_candidate_info_t *info_out) {
|
||||
struct stat param_st;
|
||||
|
||||
if (is_pfsc_image_mount_base_or_child(full_path))
|
||||
return DIRECTORY_CANDIDATE_DESCEND;
|
||||
|
||||
if (!allow_known_param_root &&
|
||||
is_under_discovered_param_root(full_path, discovered_param_roots,
|
||||
*discovered_param_root_count)) {
|
||||
return DIRECTORY_CANDIDATE_SKIP_DESCEND;
|
||||
}
|
||||
|
||||
if (is_under_image_mount_base(full_path) && !is_active_image_mount_point(full_path)) {
|
||||
log_debug(" [SKIP] inactive mount path: %s", full_path);
|
||||
return DIRECTORY_CANDIDATE_SKIP_DESCEND;
|
||||
}
|
||||
|
||||
if (!directory_has_param_json(full_path, ¶m_st)) {
|
||||
if (is_missing_param_scan_limited(full_path)) {
|
||||
log_debug(" [SKIP] param.json retry limit reached: %s", full_path);
|
||||
} else {
|
||||
record_missing_param_failure(full_path);
|
||||
}
|
||||
return DIRECTORY_CANDIDATE_DESCEND;
|
||||
}
|
||||
|
||||
if (!get_game_info(full_path, ¶m_st, info_out->title_id,
|
||||
info_out->title_name)) {
|
||||
record_missing_param_failure(full_path);
|
||||
log_debug(" [SKIP] game info unavailable: %s", full_path);
|
||||
return DIRECTORY_CANDIDATE_SKIP_DESCEND;
|
||||
}
|
||||
|
||||
if (*discovered_param_root_count < MAX_PENDING &&
|
||||
!is_under_discovered_param_root(full_path, discovered_param_roots,
|
||||
*discovered_param_root_count)) {
|
||||
(void)strlcpy(discovered_param_roots[*discovered_param_root_count], full_path,
|
||||
MAX_PATH);
|
||||
(*discovered_param_root_count)++;
|
||||
}
|
||||
clear_missing_param_entry(full_path);
|
||||
return DIRECTORY_CANDIDATE_READY;
|
||||
}
|
||||
|
||||
static int find_scan_candidate_index_by_title_id(scan_candidate_t *candidates,
|
||||
int candidate_count,
|
||||
const char *title_id) {
|
||||
for (int i = 0; i < candidate_count; i++) {
|
||||
if (strcmp(candidates[i].title_id, title_id) == 0)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void mark_scan_candidate_manual(scan_candidate_t *candidate,
|
||||
const char *manual_source_path) {
|
||||
if (!candidate || !manual_source_path || manual_source_path[0] == '\0')
|
||||
return;
|
||||
|
||||
candidate->manual = true;
|
||||
(void)strlcpy(candidate->manual_source_path, manual_source_path,
|
||||
sizeof(candidate->manual_source_path));
|
||||
}
|
||||
|
||||
static void remove_scan_candidate_at(scan_candidate_t *candidates,
|
||||
int *candidate_count, int index) {
|
||||
int trailing_count = *candidate_count - index - 1;
|
||||
if (trailing_count > 0) {
|
||||
memmove(&candidates[index], &candidates[index + 1],
|
||||
(size_t)trailing_count * sizeof(candidates[0]));
|
||||
}
|
||||
(*candidate_count)--;
|
||||
}
|
||||
|
||||
static void notify_duplicate_scan_candidate(const char *title_id,
|
||||
const char *ignored_path,
|
||||
const char *existing_path) {
|
||||
notify_duplicate_title_once(title_id, ignored_path, existing_path);
|
||||
}
|
||||
|
||||
static existing_directory_result_t handle_existing_directory_candidate(
|
||||
const char *full_path, const scan_app_db_context_t *app_db,
|
||||
const directory_candidate_info_t *info,
|
||||
bool *installed_out, bool *in_app_db_out,
|
||||
char preferred_existing_path_out[MAX_PATH]) {
|
||||
char tracked_path[MAX_PATH];
|
||||
preferred_existing_path_out[0] = '\0';
|
||||
bool has_tracked_path =
|
||||
read_mount_link(info->title_id, tracked_path, sizeof(tracked_path));
|
||||
bool link_matches_source =
|
||||
has_tracked_path && strcmp(tracked_path, full_path) == 0;
|
||||
|
||||
if (is_blocked_ppsa_title(app_db->blocked_ppsa_titles,
|
||||
app_db->blocked_ppsa_titles_ready,
|
||||
info->title_id)) {
|
||||
request_blocked_ppsa_uninstall(info->title_id, full_path);
|
||||
return EXISTING_DIRECTORY_HANDLED;
|
||||
}
|
||||
|
||||
if (!app_db->titles_ready) {
|
||||
if (link_matches_source && is_data_mounted(info->title_id)) {
|
||||
cache_game_entry(full_path, info->title_id, info->title_name);
|
||||
return EXISTING_DIRECTORY_PREFER_CURRENT;
|
||||
}
|
||||
return EXISTING_DIRECTORY_HANDLED;
|
||||
}
|
||||
|
||||
bool in_app_db = app_db_title_list_contains(app_db->titles, info->title_id);
|
||||
bool installed = in_app_db && is_installed(info->title_id);
|
||||
bool appmeta_present =
|
||||
installed ? get_appmeta_present_for_scan_cycle(info->title_id) : false;
|
||||
link_matches_source = installed && link_matches_source;
|
||||
bool source_valid = false;
|
||||
if (link_matches_source) {
|
||||
source_valid = is_data_mounted(info->title_id);
|
||||
if (!source_valid && mount_title_nullfs(info->title_id, full_path))
|
||||
source_valid = is_data_mounted(info->title_id);
|
||||
}
|
||||
|
||||
if (source_valid && appmeta_present) {
|
||||
cache_game_entry(full_path, info->title_id, info->title_name);
|
||||
return EXISTING_DIRECTORY_PREFER_CURRENT;
|
||||
}
|
||||
|
||||
if (installed && appmeta_present && has_tracked_path &&
|
||||
strcmp(tracked_path, full_path) != 0 &&
|
||||
is_data_mounted(info->title_id)) {
|
||||
(void)strlcpy(preferred_existing_path_out, tracked_path, MAX_PATH);
|
||||
cache_game_entry(tracked_path, info->title_id, info->title_name);
|
||||
return EXISTING_DIRECTORY_PREFER_CACHED;
|
||||
}
|
||||
|
||||
if (!in_app_db && is_title_install_pending(info->title_id))
|
||||
return EXISTING_DIRECTORY_HANDLED;
|
||||
|
||||
if (!in_app_db && was_register_attempted(info->title_id)) {
|
||||
return EXISTING_DIRECTORY_HANDLED;
|
||||
}
|
||||
|
||||
*installed_out = installed;
|
||||
*in_app_db_out = in_app_db;
|
||||
return EXISTING_DIRECTORY_CONTINUE;
|
||||
}
|
||||
|
||||
static bool enqueue_directory_candidate(
|
||||
const char *full_path, scan_candidate_t *candidates, int max_candidates,
|
||||
int *candidate_count, const directory_candidate_info_t *info, bool installed,
|
||||
bool in_app_db, const char *manual_source_path,
|
||||
bool *unstable_found_out) {
|
||||
char metadata_path[MAX_PATH];
|
||||
uint8_t failed_attempts = get_failed_mount_attempts(info->title_id);
|
||||
if (failed_attempts >= MAX_FAILED_MOUNT_ATTEMPTS) {
|
||||
log_debug(" [SKIP] mount/register retry limit reached (%u/%u): %s (%s)",
|
||||
(unsigned)failed_attempts, (unsigned)MAX_FAILED_MOUNT_ATTEMPTS,
|
||||
info->title_name, info->title_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
int written =
|
||||
snprintf(metadata_path, sizeof(metadata_path), "%s/sce_sys", full_path);
|
||||
if (written < 0 || (size_t)written >= sizeof(metadata_path)) {
|
||||
log_debug(" [SKIP] metadata path too long: %s (%s)", info->title_name,
|
||||
full_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!wait_for_stability_fast(metadata_path, info->title_name)) {
|
||||
if (unstable_found_out)
|
||||
*unstable_found_out = true;
|
||||
log_debug(" [SKIP] source not stable yet: %s (%s)", info->title_name,
|
||||
full_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (*candidate_count >= max_candidates) {
|
||||
log_debug(" [SKIP] candidate queue full (%d): %s (%s)", max_candidates,
|
||||
info->title_name, info->title_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
(void)strlcpy(candidates[*candidate_count].path, full_path,
|
||||
sizeof(candidates[*candidate_count].path));
|
||||
(void)strlcpy(candidates[*candidate_count].title_id, info->title_id,
|
||||
sizeof(candidates[*candidate_count].title_id));
|
||||
(void)strlcpy(candidates[*candidate_count].title_name, info->title_name,
|
||||
sizeof(candidates[*candidate_count].title_name));
|
||||
candidates[*candidate_count].manual_source_path[0] = '\0';
|
||||
candidates[*candidate_count].installed = installed;
|
||||
candidates[*candidate_count].in_app_db = in_app_db;
|
||||
candidates[*candidate_count].manual = false;
|
||||
mark_scan_candidate_manual(&candidates[*candidate_count], manual_source_path);
|
||||
(*candidate_count)++;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool try_collect_candidate_for_directory(
|
||||
const char *full_path, scan_candidate_t *candidates, int max_candidates,
|
||||
int *candidate_count, const scan_app_db_context_t *app_db,
|
||||
char discovered_param_roots[][MAX_PATH],
|
||||
int *discovered_param_root_count, bool allow_known_param_root,
|
||||
const char *manual_source_path, bool *unstable_found_out) {
|
||||
directory_candidate_info_t info;
|
||||
directory_candidate_probe_t probe_result =
|
||||
probe_directory_candidate(full_path, discovered_param_roots,
|
||||
discovered_param_root_count,
|
||||
allow_known_param_root, &info);
|
||||
|
||||
if (probe_result == DIRECTORY_CANDIDATE_SKIP_DESCEND)
|
||||
return true;
|
||||
if (probe_result == DIRECTORY_CANDIDATE_DESCEND)
|
||||
return false;
|
||||
|
||||
int duplicate_candidate_index = find_scan_candidate_index_by_title_id(
|
||||
candidates, *candidate_count, info.title_id);
|
||||
const char *duplicate_candidate_path =
|
||||
duplicate_candidate_index >= 0 ? candidates[duplicate_candidate_index].path
|
||||
: NULL;
|
||||
bool duplicate_candidate_same_path =
|
||||
duplicate_candidate_path && strcmp(duplicate_candidate_path, full_path) == 0;
|
||||
bool installed = false;
|
||||
bool in_app_db = false;
|
||||
char preferred_existing_path[MAX_PATH];
|
||||
existing_directory_result_t existing_result =
|
||||
handle_existing_directory_candidate(full_path, app_db, &info,
|
||||
&installed, &in_app_db,
|
||||
preferred_existing_path);
|
||||
if (existing_result == EXISTING_DIRECTORY_PREFER_CURRENT) {
|
||||
if (manual_source_path)
|
||||
sm_manual_note_installed(manual_source_path, info.title_id,
|
||||
info.title_name);
|
||||
if (duplicate_candidate_index >= 0) {
|
||||
if (!duplicate_candidate_same_path)
|
||||
notify_duplicate_scan_candidate(info.title_id, duplicate_candidate_path,
|
||||
full_path);
|
||||
remove_scan_candidate_at(candidates, candidate_count,
|
||||
duplicate_candidate_index);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (existing_result == EXISTING_DIRECTORY_PREFER_CACHED) {
|
||||
notify_duplicate_scan_candidate(info.title_id, full_path,
|
||||
preferred_existing_path);
|
||||
if (duplicate_candidate_index >= 0)
|
||||
remove_scan_candidate_at(candidates, candidate_count,
|
||||
duplicate_candidate_index);
|
||||
return true;
|
||||
}
|
||||
if (existing_result == EXISTING_DIRECTORY_HANDLED) {
|
||||
if (duplicate_candidate_index >= 0 && !duplicate_candidate_same_path)
|
||||
notify_duplicate_scan_candidate(info.title_id, full_path,
|
||||
duplicate_candidate_path);
|
||||
if (duplicate_candidate_index >= 0)
|
||||
mark_scan_candidate_manual(&candidates[duplicate_candidate_index],
|
||||
manual_source_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (duplicate_candidate_index >= 0) {
|
||||
if (!duplicate_candidate_same_path)
|
||||
notify_duplicate_scan_candidate(info.title_id, full_path,
|
||||
duplicate_candidate_path);
|
||||
mark_scan_candidate_manual(&candidates[duplicate_candidate_index],
|
||||
manual_source_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
return enqueue_directory_candidate(full_path, candidates, max_candidates,
|
||||
candidate_count, &info, installed,
|
||||
in_app_db, manual_source_path,
|
||||
unstable_found_out);
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
scan_candidate_t *candidates;
|
||||
int max_candidates;
|
||||
int *candidate_count;
|
||||
const scan_app_db_context_t *app_db;
|
||||
char (*discovered_param_roots)[MAX_PATH];
|
||||
int *discovered_param_root_count;
|
||||
const char *manual_source_path;
|
||||
bool *unstable_found_out;
|
||||
} collect_candidates_walk_ctx_t;
|
||||
|
||||
static void collect_scan_candidates_from_manual_root(
|
||||
const char *scan_path, const char *manual_source_path,
|
||||
scan_candidate_t *candidates, int max_candidates, int *candidate_count,
|
||||
const scan_app_db_context_t *app_db,
|
||||
char discovered_param_roots[][MAX_PATH], int *discovered_param_root_count,
|
||||
bool *unstable_found_out);
|
||||
|
||||
static sm_scan_tree_dir_visit_t collect_candidate_directory_visit(
|
||||
const char *dir_path, unsigned int depth_from_root, void *ctx_ptr) {
|
||||
if (depth_from_root == 0u)
|
||||
return SM_SCAN_TREE_DIR_DESCEND;
|
||||
|
||||
collect_candidates_walk_ctx_t *ctx = (collect_candidates_walk_ctx_t *)ctx_ptr;
|
||||
if (try_collect_candidate_for_directory(
|
||||
dir_path, ctx->candidates, ctx->max_candidates, ctx->candidate_count,
|
||||
ctx->app_db, ctx->discovered_param_roots,
|
||||
ctx->discovered_param_root_count, false, ctx->manual_source_path,
|
||||
ctx->unstable_found_out)) {
|
||||
return SM_SCAN_TREE_DIR_SKIP_DESCEND;
|
||||
}
|
||||
|
||||
return SM_SCAN_TREE_DIR_DESCEND;
|
||||
}
|
||||
|
||||
static bool collect_candidate_image_visit(const char *image_path,
|
||||
const char *image_name,
|
||||
unsigned int depth_from_root,
|
||||
void *ctx_ptr) {
|
||||
(void)depth_from_root;
|
||||
|
||||
collect_candidates_walk_ctx_t *ctx = (collect_candidates_walk_ctx_t *)ctx_ptr;
|
||||
if (!maybe_mount_image_file(image_path, image_name, ctx->unstable_found_out))
|
||||
return true;
|
||||
|
||||
if (ctx->manual_source_path && is_pfsc_image_mount_base_or_child(image_path)) {
|
||||
char mount_point[MAX_PATH];
|
||||
get_image_mount_point_for_source(image_path, mount_point);
|
||||
collect_scan_candidates_from_manual_root(
|
||||
mount_point, ctx->manual_source_path, ctx->candidates,
|
||||
ctx->max_candidates, ctx->candidate_count, ctx->app_db,
|
||||
ctx->discovered_param_roots, ctx->discovered_param_root_count,
|
||||
ctx->unstable_found_out);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void collect_scan_candidates_from_manual_root(
|
||||
const char *scan_path, const char *manual_source_path,
|
||||
scan_candidate_t *candidates, int max_candidates, int *candidate_count,
|
||||
const scan_app_db_context_t *app_db,
|
||||
char discovered_param_roots[][MAX_PATH], int *discovered_param_root_count,
|
||||
bool *unstable_found_out) {
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
return;
|
||||
|
||||
bool pfsc_container_root = is_pfsc_image_mount_base_or_child(scan_path);
|
||||
bool try_root_candidate = !pfsc_container_root;
|
||||
if (try_root_candidate && is_under_image_mount_base(scan_path)) {
|
||||
struct stat param_st;
|
||||
try_root_candidate = directory_has_param_json(scan_path, ¶m_st);
|
||||
}
|
||||
|
||||
if (try_root_candidate) {
|
||||
if (try_collect_candidate_for_directory(
|
||||
scan_path, candidates, max_candidates, candidate_count,
|
||||
app_db, discovered_param_roots, discovered_param_root_count, true,
|
||||
manual_source_path, unstable_found_out)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
unsigned int scan_depth = get_scan_depth_for_root(scan_path);
|
||||
|
||||
collect_candidates_walk_ctx_t ctx = {
|
||||
.candidates = candidates,
|
||||
.max_candidates = max_candidates,
|
||||
.candidate_count = candidate_count,
|
||||
.app_db = app_db,
|
||||
.discovered_param_roots = discovered_param_roots,
|
||||
.discovered_param_root_count = discovered_param_root_count,
|
||||
.manual_source_path = manual_source_path,
|
||||
.unstable_found_out = unstable_found_out,
|
||||
};
|
||||
sm_scan_tree_callbacks_t callbacks = {
|
||||
.on_directory = collect_candidate_directory_visit,
|
||||
.on_image_file = pfsc_container_root ? collect_candidate_image_visit
|
||||
: NULL,
|
||||
};
|
||||
(void)sm_scan_tree_walk(scan_path, scan_path, 0u, scan_depth, &callbacks, &ctx);
|
||||
}
|
||||
|
||||
static void collect_scan_candidates_from_manual_path(
|
||||
const char *manual_path, scan_candidate_t *candidates, int max_candidates,
|
||||
int *candidate_count, const scan_app_db_context_t *app_db,
|
||||
char discovered_param_roots[][MAX_PATH],
|
||||
int *discovered_param_root_count, bool *unstable_found_out) {
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
return;
|
||||
|
||||
struct stat st;
|
||||
if (stat(manual_path, &st) != 0) {
|
||||
if (note_manual_missing_source_once(manual_path))
|
||||
log_debug(" [MANUAL] source unavailable: %s", manual_path);
|
||||
return;
|
||||
}
|
||||
clear_manual_missing_source(manual_path);
|
||||
|
||||
const char *name = get_filename_component(manual_path);
|
||||
if (S_ISREG(st.st_mode) &&
|
||||
is_supported_image_file_path(manual_path, name)) {
|
||||
if (!maybe_mount_image_file(manual_path, name, unstable_found_out))
|
||||
return;
|
||||
|
||||
char mount_point[MAX_PATH];
|
||||
get_image_mount_point_for_source(manual_path, mount_point);
|
||||
collect_scan_candidates_from_manual_root(
|
||||
mount_point, manual_path, candidates, max_candidates, candidate_count,
|
||||
app_db, discovered_param_roots, discovered_param_root_count,
|
||||
unstable_found_out);
|
||||
return;
|
||||
}
|
||||
|
||||
struct stat param_st;
|
||||
if (!S_ISDIR(st.st_mode) || !directory_has_param_json(manual_path, ¶m_st)) {
|
||||
log_debug(" [MANUAL] expected game directory or image file: %s",
|
||||
manual_path);
|
||||
return;
|
||||
}
|
||||
|
||||
collect_scan_candidates_from_manual_root(
|
||||
manual_path, manual_path, candidates, max_candidates, candidate_count,
|
||||
app_db, discovered_param_roots, discovered_param_root_count,
|
||||
unstable_found_out);
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
scan_candidate_t *candidates;
|
||||
int max_candidates;
|
||||
int *candidate_count;
|
||||
const scan_app_db_context_t *app_db;
|
||||
char (*discovered_param_roots)[MAX_PATH];
|
||||
int *discovered_param_root_count;
|
||||
bool *unstable_found_out;
|
||||
int path_count;
|
||||
} manual_scan_ctx_t;
|
||||
|
||||
static bool collect_manual_path_visit(const char *manual_path, void *ctx_ptr) {
|
||||
manual_scan_ctx_t *ctx = (manual_scan_ctx_t *)ctx_ptr;
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
return false;
|
||||
if (*ctx->candidate_count >= ctx->max_candidates) {
|
||||
log_debug(" [MANUAL] candidate queue full (%d), remaining sources deferred",
|
||||
ctx->max_candidates);
|
||||
return false;
|
||||
}
|
||||
|
||||
ctx->path_count++;
|
||||
collect_scan_candidates_from_manual_path(
|
||||
manual_path, ctx->candidates, ctx->max_candidates, ctx->candidate_count,
|
||||
ctx->app_db, ctx->discovered_param_roots,
|
||||
ctx->discovered_param_root_count, ctx->unstable_found_out);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool build_backport_path_for_scan_root(const char *title_id,
|
||||
const char *scan_path,
|
||||
char backport_path[MAX_PATH]) {
|
||||
if (!title_id || title_id[0] == '\0' || !scan_path || scan_path[0] == '\0')
|
||||
return false;
|
||||
|
||||
char backport_root[MAX_PATH];
|
||||
if (!build_backports_root_path(scan_path, backport_root))
|
||||
return false;
|
||||
|
||||
int written = snprintf(backport_path, MAX_PATH, "%s/%s", backport_root,
|
||||
title_id);
|
||||
return written > 0 && (size_t)written < MAX_PATH;
|
||||
}
|
||||
|
||||
static bool backport_path_is_dir(const char *backport_path) {
|
||||
struct stat st;
|
||||
return backport_path && stat(backport_path, &st) == 0 &&
|
||||
S_ISDIR(st.st_mode);
|
||||
}
|
||||
|
||||
static bool resolve_backport_path(const char *title_id,
|
||||
const char *owning_scan_path,
|
||||
char backport_path[MAX_PATH]) {
|
||||
if (!title_id || title_id[0] == '\0')
|
||||
return false;
|
||||
|
||||
backport_path[0] = '\0';
|
||||
if (build_backport_path_for_scan_root(title_id, owning_scan_path,
|
||||
backport_path) &&
|
||||
backport_path_is_dir(backport_path)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int i = 0; i < get_scan_path_count(); i++) {
|
||||
const char *scan_path = get_scan_path(i);
|
||||
if (owning_scan_path && strcmp(scan_path, owning_scan_path) == 0)
|
||||
continue;
|
||||
|
||||
char candidate_path[MAX_PATH];
|
||||
if (!build_backport_path_for_scan_root(title_id, scan_path, candidate_path))
|
||||
continue;
|
||||
if (!backport_path_is_dir(candidate_path))
|
||||
continue;
|
||||
|
||||
(void)strlcpy(backport_path, candidate_path, MAX_PATH);
|
||||
return true;
|
||||
}
|
||||
|
||||
backport_path[0] = '\0';
|
||||
return false;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
bool *unstable_found_out;
|
||||
} backport_overlay_ctx_t;
|
||||
|
||||
static bool mount_backport_overlay_for_cached_game(const char *source_path,
|
||||
const char *title_id,
|
||||
const char *title_name,
|
||||
const char *owning_scan_root,
|
||||
void *ctx_ptr) {
|
||||
(void)title_name;
|
||||
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
return false;
|
||||
|
||||
backport_overlay_ctx_t *ctx = (backport_overlay_ctx_t *)ctx_ptr;
|
||||
char backport_path[MAX_PATH];
|
||||
if (!resolve_backport_path(title_id, owning_scan_root, backport_path)) {
|
||||
return true;
|
||||
}
|
||||
if (is_backport_mount_blocked(backport_path))
|
||||
return true;
|
||||
|
||||
bool overlay_active = false;
|
||||
if (!reconcile_title_backport_mount(title_id, source_path, backport_path,
|
||||
&overlay_active) ||
|
||||
overlay_active) {
|
||||
return true;
|
||||
}
|
||||
if (!wait_for_stability_fast(backport_path, "BKP")) {
|
||||
if (ctx->unstable_found_out)
|
||||
*ctx->unstable_found_out = true;
|
||||
return true;
|
||||
}
|
||||
overlay_active = false;
|
||||
if (!reconcile_title_backport_mount(title_id, source_path, backport_path,
|
||||
&overlay_active) ||
|
||||
overlay_active) {
|
||||
return true;
|
||||
}
|
||||
|
||||
char system_ex_path[MAX_PATH];
|
||||
snprintf(system_ex_path, sizeof(system_ex_path), "/system_ex/app/%s",
|
||||
title_id);
|
||||
if (!mount_backport_overlay(system_ex_path, backport_path, title_id)) {
|
||||
note_backport_mount_failure(backport_path);
|
||||
return true;
|
||||
}
|
||||
clear_backport_mount_failure(backport_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
void mount_backport_overlays(bool *unstable_found_out) {
|
||||
backport_overlay_ctx_t ctx = {
|
||||
.unstable_found_out = unstable_found_out,
|
||||
};
|
||||
for_each_cached_game_entry(NULL, mount_backport_overlay_for_cached_game, &ctx);
|
||||
}
|
||||
|
||||
// --- Unified Scan Pass (images + game candidates) ---
|
||||
void cleanup_lost_sources_before_scan(void) {
|
||||
// 1) Drop stale game cache entries for deleted sources.
|
||||
prune_game_cache();
|
||||
// 2) Drop stale/broken mount links and unmount stale /system_ex stacks.
|
||||
cleanup_mount_links(NULL, true);
|
||||
// 3) Unmount stale image mounts for deleted image files.
|
||||
cleanup_stale_image_mounts();
|
||||
// 4) Drop stale path-state entries.
|
||||
prune_path_state();
|
||||
}
|
||||
|
||||
void cleanup_lost_sources_for_scan_root(const char *scan_root) {
|
||||
prune_game_cache_for_root(scan_root);
|
||||
cleanup_mount_links(scan_root, true);
|
||||
cleanup_stale_image_mounts_for_root(scan_root);
|
||||
prune_path_state_for_root(scan_root);
|
||||
}
|
||||
|
||||
void unmount_usb_sources_for_suspend(void) {
|
||||
log_debug("[SLEEP] USB-backed mount cleanup begin");
|
||||
cleanup_usb_mount_links_for_suspend();
|
||||
if (!unmount_usb_image_mounts_for_suspend()) {
|
||||
log_debug("[SLEEP] some USB-backed image mounts remain busy");
|
||||
}
|
||||
cleanup_mount_dirs();
|
||||
log_debug("[SLEEP] USB-backed mount cleanup done");
|
||||
}
|
||||
|
||||
static void collect_scan_candidates_from_root(
|
||||
const char *scan_path, scan_candidate_t *candidates, int max_candidates,
|
||||
int *candidate_count, const scan_app_db_context_t *app_db,
|
||||
char discovered_param_roots[][MAX_PATH],
|
||||
int *discovered_param_root_count, bool *unstable_found_out) {
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
return;
|
||||
|
||||
unsigned int scan_depth = get_scan_depth_for_root(scan_path);
|
||||
|
||||
collect_candidates_walk_ctx_t ctx = {
|
||||
.candidates = candidates,
|
||||
.max_candidates = max_candidates,
|
||||
.candidate_count = candidate_count,
|
||||
.app_db = app_db,
|
||||
.discovered_param_roots = discovered_param_roots,
|
||||
.discovered_param_root_count = discovered_param_root_count,
|
||||
.manual_source_path = NULL,
|
||||
.unstable_found_out = unstable_found_out,
|
||||
};
|
||||
sm_scan_tree_callbacks_t callbacks = {
|
||||
.on_directory = collect_candidate_directory_visit,
|
||||
.on_image_file = collect_candidate_image_visit,
|
||||
};
|
||||
(void)sm_scan_tree_walk(scan_path, scan_path, 0u, scan_depth, &callbacks, &ctx);
|
||||
}
|
||||
|
||||
static void collect_scan_candidates_from_manual_list(
|
||||
scan_candidate_t *candidates, int max_candidates, int *candidate_count,
|
||||
const scan_app_db_context_t *app_db,
|
||||
char discovered_param_roots[][MAX_PATH], int *discovered_param_root_count,
|
||||
bool *unstable_found_out) {
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
return;
|
||||
|
||||
(void)sm_manual_reconcile_deleted_titles(app_db->titles,
|
||||
app_db->titles_ready);
|
||||
|
||||
manual_scan_ctx_t ctx = {
|
||||
.candidates = candidates,
|
||||
.max_candidates = max_candidates,
|
||||
.candidate_count = candidate_count,
|
||||
.app_db = app_db,
|
||||
.discovered_param_roots = discovered_param_roots,
|
||||
.discovered_param_root_count = discovered_param_root_count,
|
||||
.unstable_found_out = unstable_found_out,
|
||||
.path_count = 0,
|
||||
};
|
||||
if (!sm_manual_for_each_path(collect_manual_path_visit, &ctx) ||
|
||||
ctx.path_count <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
log_debug(" [MANUAL] scanned %d manual source(s)", ctx.path_count);
|
||||
}
|
||||
|
||||
static void uninstall_blocked_ppsa_titles(
|
||||
const struct AppDbTitleList *blocked_ppsa_titles,
|
||||
bool blocked_ppsa_titles_ready) {
|
||||
if (!blocked_ppsa_titles_ready || !blocked_ppsa_titles)
|
||||
return;
|
||||
|
||||
int requested = 0;
|
||||
for (int i = 0; i < blocked_ppsa_titles->count &&
|
||||
!should_stop_requested() && !runtime_sleep_mode_active();
|
||||
i++) {
|
||||
const char *title_id = blocked_ppsa_titles->ids[i];
|
||||
if (request_blocked_ppsa_uninstall(title_id, NULL))
|
||||
requested++;
|
||||
}
|
||||
|
||||
if (requested > 0)
|
||||
log_debug(" [REG] requested %d blocked PPSA uninstall(s)", requested);
|
||||
}
|
||||
|
||||
int collect_scan_candidates_for_scan_root(const char *scan_root,
|
||||
scan_candidate_t *candidates,
|
||||
int max_candidates,
|
||||
int *total_found_out,
|
||||
bool *unstable_found_out) {
|
||||
reset_scan_workspace();
|
||||
int candidate_count = 0;
|
||||
struct AppDbTitleList app_db_titles = {0};
|
||||
struct AppDbTitleList blocked_ppsa_titles = {0};
|
||||
bool app_db_titles_ready = get_app_db_title_list_cached(&app_db_titles);
|
||||
bool blocked_ppsa_titles_ready =
|
||||
get_app_db_blocked_uninstall_ppsa_list(&blocked_ppsa_titles);
|
||||
int discovered_param_root_count = 0;
|
||||
|
||||
if (!app_db_titles_ready)
|
||||
log_debug(" [DB] app.db title list unavailable for this scan cycle");
|
||||
if (!blocked_ppsa_titles_ready)
|
||||
log_debug(" [DB] blocked PPSA uninstall list unavailable for this scan cycle");
|
||||
|
||||
scan_app_db_context_t app_db = {
|
||||
.titles = &app_db_titles,
|
||||
.blocked_ppsa_titles = &blocked_ppsa_titles,
|
||||
.titles_ready = app_db_titles_ready,
|
||||
.blocked_ppsa_titles_ready = blocked_ppsa_titles_ready,
|
||||
};
|
||||
collect_scan_candidates_from_root(scan_root, candidates, max_candidates,
|
||||
&candidate_count, &app_db,
|
||||
g_scan_workspace.discovered_param_roots,
|
||||
&discovered_param_root_count,
|
||||
unstable_found_out);
|
||||
|
||||
if (total_found_out)
|
||||
*total_found_out = discovered_param_root_count;
|
||||
free_app_db_title_list(&blocked_ppsa_titles);
|
||||
free_app_db_title_list(&app_db_titles);
|
||||
return candidate_count;
|
||||
}
|
||||
|
||||
int collect_scan_candidates(scan_candidate_t *candidates, int max_candidates,
|
||||
int *total_found_out,
|
||||
bool *unstable_found_out) {
|
||||
reset_scan_workspace();
|
||||
int candidate_count = 0;
|
||||
struct AppDbTitleList app_db_titles = {0};
|
||||
struct AppDbTitleList blocked_ppsa_titles = {0};
|
||||
bool app_db_titles_ready = get_app_db_title_list_cached(&app_db_titles);
|
||||
bool blocked_ppsa_titles_ready =
|
||||
get_app_db_blocked_uninstall_ppsa_list(&blocked_ppsa_titles);
|
||||
int discovered_param_root_count = 0;
|
||||
|
||||
if (!app_db_titles_ready)
|
||||
log_debug(" [DB] app.db title list unavailable for this scan cycle");
|
||||
if (!blocked_ppsa_titles_ready)
|
||||
log_debug(" [DB] blocked PPSA uninstall list unavailable for this scan cycle");
|
||||
|
||||
scan_app_db_context_t app_db = {
|
||||
.titles = &app_db_titles,
|
||||
.blocked_ppsa_titles = &blocked_ppsa_titles,
|
||||
.titles_ready = app_db_titles_ready,
|
||||
.blocked_ppsa_titles_ready = blocked_ppsa_titles_ready,
|
||||
};
|
||||
for (int i = 0; i < get_scan_path_count(); i++) {
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
break;
|
||||
collect_scan_candidates_from_root(get_scan_path(i), candidates,
|
||||
max_candidates,
|
||||
&candidate_count, &app_db,
|
||||
g_scan_workspace.discovered_param_roots,
|
||||
&discovered_param_root_count,
|
||||
unstable_found_out);
|
||||
}
|
||||
|
||||
collect_scan_candidates_from_manual_list(
|
||||
candidates, max_candidates, &candidate_count, &app_db,
|
||||
g_scan_workspace.discovered_param_roots, &discovered_param_root_count,
|
||||
unstable_found_out);
|
||||
|
||||
uninstall_blocked_ppsa_titles(&blocked_ppsa_titles,
|
||||
blocked_ppsa_titles_ready);
|
||||
|
||||
if (total_found_out)
|
||||
*total_found_out = discovered_param_root_count;
|
||||
free_app_db_title_list(&blocked_ppsa_titles);
|
||||
free_app_db_title_list(&app_db_titles);
|
||||
return candidate_count;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
#include "sm_platform.h"
|
||||
#include "sm_scan_tree.h"
|
||||
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_filesystem.h"
|
||||
#include "sm_image.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_paths.h"
|
||||
#include "sm_runtime.h"
|
||||
|
||||
static void classify_scan_tree_entry(const char *full_path, unsigned char d_type,
|
||||
bool *is_dir_out, bool *is_regular_out) {
|
||||
bool is_dir = false;
|
||||
bool is_regular = false;
|
||||
|
||||
if (d_type == DT_DIR) {
|
||||
is_dir = true;
|
||||
} else if (d_type == DT_REG) {
|
||||
is_regular = true;
|
||||
} else if (d_type == DT_UNKNOWN) {
|
||||
struct stat st;
|
||||
if (lstat(full_path, &st) == 0) {
|
||||
is_dir = S_ISDIR(st.st_mode);
|
||||
is_regular = S_ISREG(st.st_mode);
|
||||
}
|
||||
}
|
||||
|
||||
*is_dir_out = is_dir;
|
||||
*is_regular_out = is_regular;
|
||||
}
|
||||
|
||||
static bool is_distinct_configured_scan_root(const char *current_scan_root,
|
||||
const char *path) {
|
||||
for (int i = 0; i < get_scan_path_count(); i++) {
|
||||
const char *scan_path = get_scan_path(i);
|
||||
if (strcmp(scan_path, path) != 0)
|
||||
continue;
|
||||
if (strcmp(current_scan_root, path) == 0)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool sm_scan_tree_walk(const char *scan_root, const char *dir_path,
|
||||
unsigned int depth_from_root,
|
||||
unsigned int remaining_depth,
|
||||
const sm_scan_tree_callbacks_t *callbacks, void *ctx) {
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
return true;
|
||||
|
||||
if (depth_from_root > 0u &&
|
||||
is_distinct_configured_scan_root(scan_root, dir_path)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
sm_scan_tree_dir_visit_t dir_visit = SM_SCAN_TREE_DIR_DESCEND;
|
||||
if (callbacks->on_directory) {
|
||||
dir_visit = callbacks->on_directory(dir_path, depth_from_root, ctx);
|
||||
if (dir_visit == SM_SCAN_TREE_DIR_ABORT)
|
||||
return false;
|
||||
if (dir_visit == SM_SCAN_TREE_DIR_SKIP_DESCEND)
|
||||
return true;
|
||||
}
|
||||
|
||||
if (remaining_depth == 0u)
|
||||
return true;
|
||||
|
||||
DIR *d = opendir(dir_path);
|
||||
if (!d)
|
||||
return true;
|
||||
|
||||
bool scan_root_is_pfsc_mount_base =
|
||||
is_pfsc_image_mount_base_or_child(scan_root);
|
||||
bool skip_backports_root =
|
||||
(depth_from_root == 0u && !is_under_image_mount_base(scan_root));
|
||||
bool allow_image_file_visits =
|
||||
callbacks->on_image_file &&
|
||||
(!path_matches_root_or_child(scan_root, IMAGE_MOUNT_BASE) ||
|
||||
scan_root_is_pfsc_mount_base);
|
||||
|
||||
struct dirent *entry;
|
||||
while ((entry = readdir(d)) != NULL) {
|
||||
if (should_stop_requested() || runtime_sleep_mode_active()) {
|
||||
closedir(d);
|
||||
return true;
|
||||
}
|
||||
if (entry->d_name[0] == '.')
|
||||
continue;
|
||||
if (skip_backports_root &&
|
||||
strcmp(entry->d_name, DEFAULT_BACKPORTS_DIR_NAME) == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
char full_path[MAX_PATH];
|
||||
snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name);
|
||||
|
||||
bool is_dir = false;
|
||||
bool is_regular = false;
|
||||
classify_scan_tree_entry(full_path, entry->d_type, &is_dir, &is_regular);
|
||||
|
||||
if (allow_image_file_visits && is_regular &&
|
||||
is_supported_image_file_path(full_path, entry->d_name)) {
|
||||
if (!callbacks->on_image_file(full_path, entry->d_name, depth_from_root + 1u,
|
||||
ctx)) {
|
||||
closedir(d);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_dir)
|
||||
continue;
|
||||
|
||||
if (!sm_scan_tree_walk(scan_root, full_path, depth_from_root + 1u,
|
||||
remaining_depth - 1u, callbacks, ctx)) {
|
||||
closedir(d);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
closedir(d);
|
||||
return true;
|
||||
}
|
||||
+1694
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,789 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <pthread.h>
|
||||
|
||||
#include "sm_game_lifecycle.h"
|
||||
#include "sm_kstuff.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_scan.h"
|
||||
#include "sm_shellcore_flags.h"
|
||||
|
||||
#define SHELLCORE_FLAG_WAITMODE_OR 2u
|
||||
#define SHELLCORE_FLAG_ALL_BITS UINT64_MAX
|
||||
#define SYSTEM_STATE_MGR_STATUS_SHELLUI_SHUTDOWN_IN_PROGRESS 0x0000000000200000ULL
|
||||
|
||||
typedef intptr_t sm_kernel_event_flag_t;
|
||||
|
||||
int sceKernelOpenEventFlag(sm_kernel_event_flag_t *ef, const char *name);
|
||||
int sceKernelPollEventFlag(sm_kernel_event_flag_t ef, uint64_t bit_pattern,
|
||||
unsigned int wait_mode, uint64_t *result_pattern);
|
||||
int sceKernelCloseEventFlag(sm_kernel_event_flag_t ef);
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
sm_kernel_event_flag_t handle;
|
||||
uint64_t last_pattern;
|
||||
int last_rc;
|
||||
bool required;
|
||||
bool is_open;
|
||||
bool has_last_pattern;
|
||||
bool has_last_rc;
|
||||
} shellcore_flag_monitor_t;
|
||||
|
||||
typedef struct {
|
||||
uint64_t mask;
|
||||
const char *name;
|
||||
} shellcore_flag_bit_desc_t;
|
||||
|
||||
static const shellcore_flag_bit_desc_t g_lnc_util_system_status_bits[] = {
|
||||
{0x0000000000000001ULL, "EXTRA_AUDIO_CPU_BUDGET_AVAILABLE"},
|
||||
{0x0000000000000002ULL, "SHELLUI_FG_GAME_BG_CPU_MODE"},
|
||||
};
|
||||
|
||||
static shellcore_flag_monitor_t g_shellcore_flags[] = {
|
||||
{.name = "SceShellCoreUtilAppFocus", .handle = -1, .required = true},
|
||||
{.name = "SceSystemStateMgrInfo", .handle = -1, .required = true},
|
||||
{.name = "SceLncUtilSystemStatus", .handle = -1, .required = false},
|
||||
{.name = "SceSystemStateMgrStatus", .handle = -1},
|
||||
/* {.name = "SceSysCoreSuspend", .handle = -1},
|
||||
{.name = "SceShellCoreUtilPowerControl", .handle = -1},
|
||||
{.name = "SceShellCoreUtilRunLevel", .handle = -1},
|
||||
{.name = "SceShellCoreUtilUIStatus", .handle = -1},
|
||||
{.name = "SceAutoMountUsbMass", .handle = -1},
|
||||
{.name = "SceVshctlInternalUserFlag", .handle = -1},
|
||||
{.name = "SceBootStatusFlags", .handle = -1},
|
||||
{.name = "SceSysCoreReboot", .handle = -1},
|
||||
*/
|
||||
};
|
||||
|
||||
static pthread_t g_shellcore_flag_thread;
|
||||
static pthread_mutex_t g_shellcore_flag_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_cond_t g_shellcore_flag_cond = PTHREAD_COND_INITIALIZER;
|
||||
static bool g_shellcore_flag_thread_started = false;
|
||||
static bool g_shellcore_flag_start_ready = false;
|
||||
static bool g_shellcore_flag_start_success = false;
|
||||
static volatile sig_atomic_t g_shellcore_flag_stop_requested = 0;
|
||||
|
||||
static const shellcore_flag_bit_desc_t g_system_state_mgr_status_bits[] = {
|
||||
{0x0000000000000001ULL, "CEC_ONE_TOUCH_PLAY_COMMAND"},
|
||||
{0x0000000000000002ULL, "MEDIA_PLAYBACK_MODE"},
|
||||
{0x0000000000000004ULL, "BD_DRIVE_READY"},
|
||||
{0x0000000000000008ULL, "TICK_MUSIC_PLAYBACK"},
|
||||
{0x0000000000000010ULL, "TICK_VIDEO_PLAYBACK"},
|
||||
{0x0000000000000020ULL, "TICK_0x20"},
|
||||
{0x0000000000000040ULL, "TICK_PARTY_CHAT"},
|
||||
{0x0000000000000100ULL, "TURN_OFF_REQUEST"},
|
||||
{0x0000000000000200ULL, "REBOOT_REQUEST"},
|
||||
{0x0000000000000400ULL, "ENTER_STANDBY_REQUEST"},
|
||||
{0x0000000000010000ULL, "START_SHUTDOWN_TIMER"},
|
||||
{0x0000000000020000ULL, "START_REBOOT_TIMER"},
|
||||
{0x0000000000040000ULL, "START_STANDBY_TIMER"},
|
||||
{0x0000000000080000ULL, "EXTEND_SHUTDOWN_TIMER"},
|
||||
{0x0000000000100000ULL, "CANCEL_SHUTDOWN_TIMER"},
|
||||
{SYSTEM_STATE_MGR_STATUS_SHELLUI_SHUTDOWN_IN_PROGRESS,
|
||||
"SHELLUI_SHUTDOWN_IN_PROGRESS"},
|
||||
{0x0000000000400000ULL, "EXTEND_SHUTDOWN_TIMER_POST_AUTOUPDATE"},
|
||||
{0x0000000001000000ULL, "BIGAPP_LAUNCH_READY"},
|
||||
{0x0000000004000000ULL, "VSH_AUTO_UPDATE_VERIFY_DONE"},
|
||||
{0x0000000008000000ULL, "START_REBOOT_TIMER_FAST"},
|
||||
};
|
||||
|
||||
#if 0
|
||||
static const shellcore_flag_bit_desc_t g_power_control_bits[] = {
|
||||
{0x0000000000020000ULL, "SCREEN_SAVER_ON"},
|
||||
{0x0000000000040000ULL, "POWER_TRANSITION"},
|
||||
{0x0000000000080000ULL, "POWER_LOCK"},
|
||||
{0x0000000000100000ULL, "SOCIAL_SCREEN_STATUS"},
|
||||
{0x0000000000200000ULL, "REQUEST_CAMERA_CALIBRATION"},
|
||||
{0x0000000002000000ULL, "USER_INPUT_PULSE"},
|
||||
{0x0000000004000000ULL, "IMPOSE_OVERLAY"},
|
||||
{0x0000000008000000ULL, "AUTO_POWER_DOWN_TIMER_RESET"},
|
||||
{0x0000000010000000ULL, "REMOTE_PLAY_STATUS"},
|
||||
{0x0000000020000000ULL, "SHARE_PLAY_STATUS"},
|
||||
{0x0000000040000000ULL, "GAME_LIVE_STREAMING"},
|
||||
{0x0000000080000000ULL, "BGM_CPU_BUDGET"},
|
||||
{0x0000000100000000ULL, "PRENOTIFY_REMOTE_PLAY"},
|
||||
{0x0000000200000000ULL, "PRENOTIFY_SHARE_PLAY"},
|
||||
{0x0000000400000000ULL, "PRENOTIFY_GAME_LIVE_STREAMING"},
|
||||
{0x0000000800000000ULL, "REMOTE_PLAY_CPU_BUDGET"},
|
||||
{0x0000001000000000ULL, "SHARE_PLAY_CPU_BUDGET"},
|
||||
{0x0000100000000000ULL, "EXT_STORAGE_APP_MOVE_IN_PROGRESS"},
|
||||
{0x0000200000000000ULL, "STATE_0x2000000000000"},
|
||||
};
|
||||
|
||||
static const shellcore_flag_bit_desc_t g_shellcore_ui_status_bits[] = {
|
||||
{0x0000000000000001ULL, "LOW_BIT_0x1"},
|
||||
{0x0000000000000010ULL, "LOW_BIT_0x10"},
|
||||
{0x0000000000018000ULL, "BASIC_PRODUCT_SHAPE_FIELD"},
|
||||
{0x0000000000020000ULL, "EXT_HDD_MOUNT_UNBLOCK"},
|
||||
{0x0000000400000000ULL, "HIGH32_BIT2"},
|
||||
};
|
||||
|
||||
static const shellcore_flag_bit_desc_t g_vshctl_internal_user_bits[] = {
|
||||
{0x0000000000000001ULL, "INTERNAL_CRONOS_USER"},
|
||||
};
|
||||
|
||||
|
||||
static const shellcore_flag_bit_desc_t g_boot_status_bits[] = {
|
||||
{0x0000000000000002ULL, "BIT_0x2"},
|
||||
{0x0000000000000040ULL, "BIT_0x40"},
|
||||
{0x0000000000000080ULL, "BIT_0x80"},
|
||||
{0x0000000000000200ULL, "BIT_0x200"},
|
||||
{0x0000000000004000ULL, "BIT_0x4000"},
|
||||
{0x0000000001000000ULL, "BIGAPP_LAUNCH_READY"},
|
||||
};
|
||||
|
||||
static const shellcore_flag_bit_desc_t g_syscore_reboot_bits[] = {
|
||||
{0x0000000000000040ULL, "BIT_0x40"},
|
||||
{0x0000000000004000ULL, "BIT_0x4000"},
|
||||
{0x0000000100000000ULL, "REBOOT_FLAG"},
|
||||
};
|
||||
|
||||
static const shellcore_flag_bit_desc_t g_syscore_suspend_bits[] = {
|
||||
{0x0000000000000001ULL, "SUSPEND_ACK"},
|
||||
{0x0000000000000002ULL, "SUSPEND_REQ_BIT1"},
|
||||
{0x0000000000000004ULL, "SUSPEND_REQ_BIT2"},
|
||||
{0x0000000000000008ULL, "SUSPEND_REQ_BIT3"},
|
||||
};
|
||||
#endif
|
||||
|
||||
enum {
|
||||
SYSTEM_STATE_MGR_STATE_INVALID = 0u,
|
||||
SYSTEM_STATE_MGR_STATE_INITIALIZING = 10u,
|
||||
SYSTEM_STATE_MGR_STATE_SHUTDOWN_ON_GOING = 100u,
|
||||
SYSTEM_STATE_MGR_STATE_POWER_SAVING = 200u,
|
||||
SYSTEM_STATE_MGR_STATE_SUSPEND_ON_GOING = 300u,
|
||||
SYSTEM_STATE_MGR_STATE_MAIN_ON_STANDBY = 500u,
|
||||
SYSTEM_STATE_MGR_STATE_WORKING = 1000u,
|
||||
};
|
||||
|
||||
enum {
|
||||
SYSTEM_STATE_MGR_REBOOT_CAUSE_NONE = 0u,
|
||||
SYSTEM_STATE_MGR_REBOOT_CAUSE_CFF = 1u,
|
||||
};
|
||||
|
||||
static const shellcore_flag_bit_desc_t *get_shellcore_flag_bits(
|
||||
const shellcore_flag_monitor_t *flag, size_t *count_out) {
|
||||
if (count_out)
|
||||
*count_out = 0;
|
||||
if (!flag || !flag->name)
|
||||
return NULL;
|
||||
|
||||
if (strcmp(flag->name, "SceLncUtilSystemStatus") == 0) {
|
||||
if (count_out) {
|
||||
*count_out = sizeof(g_lnc_util_system_status_bits) /
|
||||
sizeof(g_lnc_util_system_status_bits[0]);
|
||||
}
|
||||
return g_lnc_util_system_status_bits;
|
||||
}
|
||||
|
||||
if (strcmp(flag->name, "SceSystemStateMgrStatus") == 0) {
|
||||
if (count_out) {
|
||||
*count_out = sizeof(g_system_state_mgr_status_bits) /
|
||||
sizeof(g_system_state_mgr_status_bits[0]);
|
||||
}
|
||||
return g_system_state_mgr_status_bits;
|
||||
}
|
||||
#if 0
|
||||
if (strcmp(flag->name, "SceShellCoreUtilPowerControl") == 0) {
|
||||
if (count_out)
|
||||
*count_out = sizeof(g_power_control_bits) / sizeof(g_power_control_bits[0]);
|
||||
return g_power_control_bits;
|
||||
}
|
||||
|
||||
if (strcmp(flag->name, "SceShellCoreUtilUIStatus") == 0) {
|
||||
if (count_out) {
|
||||
*count_out = sizeof(g_shellcore_ui_status_bits) /
|
||||
sizeof(g_shellcore_ui_status_bits[0]);
|
||||
}
|
||||
return g_shellcore_ui_status_bits;
|
||||
}
|
||||
|
||||
if (strcmp(flag->name, "SceVshctlInternalUserFlag") == 0) {
|
||||
if (count_out) {
|
||||
*count_out = sizeof(g_vshctl_internal_user_bits) /
|
||||
sizeof(g_vshctl_internal_user_bits[0]);
|
||||
}
|
||||
return g_vshctl_internal_user_bits;
|
||||
}
|
||||
|
||||
|
||||
if (strcmp(flag->name, "SceBootStatusFlags") == 0) {
|
||||
if (count_out)
|
||||
*count_out = sizeof(g_boot_status_bits) / sizeof(g_boot_status_bits[0]);
|
||||
return g_boot_status_bits;
|
||||
}
|
||||
|
||||
if (strcmp(flag->name, "SceSysCoreReboot") == 0) {
|
||||
if (count_out)
|
||||
*count_out = sizeof(g_syscore_reboot_bits) / sizeof(g_syscore_reboot_bits[0]);
|
||||
return g_syscore_reboot_bits;
|
||||
}
|
||||
|
||||
if (strcmp(flag->name, "SceSysCoreSuspend") == 0) {
|
||||
if (count_out)
|
||||
*count_out = sizeof(g_syscore_suspend_bits) /
|
||||
sizeof(g_syscore_suspend_bits[0]);
|
||||
return g_syscore_suspend_bits;
|
||||
}
|
||||
#endif
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static const char *get_system_state_mgr_state_name(unsigned state) {
|
||||
switch (state) {
|
||||
case SYSTEM_STATE_MGR_STATE_INVALID:
|
||||
return "INVALID";
|
||||
case SYSTEM_STATE_MGR_STATE_INITIALIZING:
|
||||
return "INITIALIZING";
|
||||
case SYSTEM_STATE_MGR_STATE_SHUTDOWN_ON_GOING:
|
||||
return "SHUTDOWN_ON_GOING";
|
||||
case SYSTEM_STATE_MGR_STATE_POWER_SAVING:
|
||||
return "POWER_SAVING";
|
||||
case SYSTEM_STATE_MGR_STATE_SUSPEND_ON_GOING:
|
||||
return "SUSPEND_ON_GOING";
|
||||
case SYSTEM_STATE_MGR_STATE_MAIN_ON_STANDBY:
|
||||
return "MAIN_ON_STANDBY";
|
||||
case SYSTEM_STATE_MGR_STATE_WORKING:
|
||||
return "WORKING";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
static const char *get_system_state_mgr_reboot_cause_name(unsigned cause) {
|
||||
switch (cause) {
|
||||
case SYSTEM_STATE_MGR_REBOOT_CAUSE_NONE:
|
||||
return "NONE";
|
||||
case SYSTEM_STATE_MGR_REBOOT_CAUSE_CFF:
|
||||
return "CFF";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
static void append_shellcore_flag_token(char *dst, size_t dst_size,
|
||||
const char *token) {
|
||||
size_t used;
|
||||
|
||||
if (!dst || dst_size == 0 || !token || token[0] == '\0')
|
||||
return;
|
||||
|
||||
used = strnlen(dst, dst_size);
|
||||
if (used >= dst_size - 1)
|
||||
return;
|
||||
|
||||
(void)snprintf(dst + used, dst_size - used, "%s%s", used == 0 ? "" : "|",
|
||||
token);
|
||||
}
|
||||
|
||||
static bool format_shellcore_flag_special_known_masks(
|
||||
const shellcore_flag_monitor_t *flag, char *dst, size_t dst_size) {
|
||||
if (!flag || !flag->name || !dst || dst_size == 0)
|
||||
return false;
|
||||
|
||||
#if 0
|
||||
if (strcmp(flag->name, "SceShellCoreUtilRunLevel") == 0) {
|
||||
append_shellcore_flag_token(dst, dst_size, "RUN_LEVEL_NUMERIC");
|
||||
append_shellcore_flag_token(dst, dst_size, "POLL_MASK=0xFFFFFFF");
|
||||
append_shellcore_flag_token(dst, dst_size, "INITIAL=10");
|
||||
append_shellcore_flag_token(dst, dst_size, "READY=100");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(flag->name, "SceAutoMountUsbMass") == 0) {
|
||||
append_shellcore_flag_token(dst, dst_size, "USB_MOUNT_BITS=0xFFFFFFF");
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (strcmp(flag->name, "SceSystemStateMgrInfo") == 0) {
|
||||
append_shellcore_flag_token(dst, dst_size,
|
||||
"CURRENT_STATE=bits[0..15]");
|
||||
append_shellcore_flag_token(dst, dst_size,
|
||||
"TRIGGER_CODE=bits[32..47]");
|
||||
append_shellcore_flag_token(dst, dst_size,
|
||||
"REBOOT_CAUSE=bits[48..63]");
|
||||
append_shellcore_flag_token(dst, dst_size, "STATE_INVALID=0");
|
||||
append_shellcore_flag_token(dst, dst_size, "STATE_INITIALIZING=10");
|
||||
append_shellcore_flag_token(dst, dst_size,
|
||||
"STATE_SHUTDOWN_ON_GOING=100");
|
||||
append_shellcore_flag_token(dst, dst_size, "STATE_POWER_SAVING=200");
|
||||
append_shellcore_flag_token(dst, dst_size,
|
||||
"STATE_SUSPEND_ON_GOING=300");
|
||||
append_shellcore_flag_token(dst, dst_size,
|
||||
"STATE_MAIN_ON_STANDBY=500");
|
||||
append_shellcore_flag_token(dst, dst_size, "STATE_WORKING=1000");
|
||||
append_shellcore_flag_token(dst, dst_size, "REBOOT_CAUSE_NONE=0");
|
||||
append_shellcore_flag_token(dst, dst_size, "REBOOT_CAUSE_CFF=1");
|
||||
append_shellcore_flag_token(dst, dst_size, "INITIAL=0x10000000A");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static void format_shellcore_flag_known_masks(const shellcore_flag_monitor_t *flag,
|
||||
char *dst, size_t dst_size) {
|
||||
size_t bit_count = 0;
|
||||
const shellcore_flag_bit_desc_t *bits = get_shellcore_flag_bits(flag, &bit_count);
|
||||
|
||||
if (!dst || dst_size == 0)
|
||||
return;
|
||||
dst[0] = '\0';
|
||||
|
||||
(void)format_shellcore_flag_special_known_masks(flag, dst, dst_size);
|
||||
|
||||
for (size_t i = 0; i < bit_count; ++i) {
|
||||
char token[96];
|
||||
(void)snprintf(token, sizeof(token), "%s=0x%llX", bits[i].name,
|
||||
(unsigned long long)bits[i].mask);
|
||||
append_shellcore_flag_token(dst, dst_size, token);
|
||||
}
|
||||
|
||||
if (strcmp(flag->name, "SceShellCoreUtilAppFocus") == 0) {
|
||||
append_shellcore_flag_token(dst, dst_size, "APP_ID");
|
||||
}
|
||||
}
|
||||
|
||||
static bool format_shellcore_flag_special_value(const shellcore_flag_monitor_t *flag,
|
||||
uint64_t pattern, char *dst,
|
||||
size_t dst_size) {
|
||||
if (!flag || !flag->name || !dst || dst_size == 0)
|
||||
return false;
|
||||
|
||||
#if 0
|
||||
|
||||
if (strcmp(flag->name, "SceShellCoreUtilRunLevel") == 0) {
|
||||
char token[96];
|
||||
(void)snprintf(token, sizeof(token), "RUN_LEVEL=%u",
|
||||
(unsigned)(pattern & 0x0FFFFFFFULL));
|
||||
append_shellcore_flag_token(dst, dst_size, token);
|
||||
if ((pattern & 0x0FFFFFFFULL) == 10u) {
|
||||
append_shellcore_flag_token(dst, dst_size, "INITIAL");
|
||||
} else if ((pattern & 0x0FFFFFFFULL) == 100u) {
|
||||
append_shellcore_flag_token(dst, dst_size, "READY");
|
||||
}
|
||||
if ((pattern & ~0x0FFFFFFFULL) != 0) {
|
||||
(void)snprintf(token, sizeof(token), "UNKNOWN=0x%llX",
|
||||
(unsigned long long)(pattern & ~0x0FFFFFFFULL));
|
||||
append_shellcore_flag_token(dst, dst_size, token);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(flag->name, "SceAutoMountUsbMass") == 0) {
|
||||
uint64_t usb_bits = pattern & 0x0FFFFFFFULL;
|
||||
for (unsigned i = 0; i < 28; ++i) {
|
||||
if ((usb_bits & (1ULL << i)) == 0)
|
||||
continue;
|
||||
char token[32];
|
||||
(void)snprintf(token, sizeof(token), "USB%u", i);
|
||||
append_shellcore_flag_token(dst, dst_size, token);
|
||||
}
|
||||
if ((pattern & ~0x0FFFFFFFULL) != 0) {
|
||||
char token[64];
|
||||
(void)snprintf(token, sizeof(token), "UNKNOWN=0x%llX",
|
||||
(unsigned long long)(pattern & ~0x0FFFFFFFULL));
|
||||
append_shellcore_flag_token(dst, dst_size, token);
|
||||
}
|
||||
if (dst[0] == '\0')
|
||||
(void)snprintf(dst, dst_size, "0");
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (strcmp(flag->name, "SceSystemStateMgrInfo") == 0) {
|
||||
unsigned current_state = (unsigned)(pattern & 0xFFFFu);
|
||||
unsigned mid16 = (unsigned)((pattern >> 16) & 0xFFFFu);
|
||||
unsigned trigger_code = (unsigned)((pattern >> 32) & 0xFFFFu);
|
||||
unsigned reboot_cause = (unsigned)((pattern >> 48) & 0xFFFFu);
|
||||
char token[128];
|
||||
|
||||
(void)snprintf(token, sizeof(token), "STATE=%s(0x%04X)",
|
||||
get_system_state_mgr_state_name(current_state), current_state);
|
||||
append_shellcore_flag_token(dst, dst_size, token);
|
||||
|
||||
(void)snprintf(token, sizeof(token), "TRIGGER=0x%04X", trigger_code);
|
||||
append_shellcore_flag_token(dst, dst_size, token);
|
||||
|
||||
(void)snprintf(token, sizeof(token), "REBOOT_CAUSE=%s(0x%04X)",
|
||||
get_system_state_mgr_reboot_cause_name(reboot_cause),
|
||||
reboot_cause);
|
||||
append_shellcore_flag_token(dst, dst_size, token);
|
||||
|
||||
if (mid16 != 0) {
|
||||
(void)snprintf(token, sizeof(token), "MID16=0x%04X", mid16);
|
||||
append_shellcore_flag_token(dst, dst_size, token);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(flag->name, "SceShellCoreUtilAppFocus") == 0) {
|
||||
unsigned low32 = (unsigned)(pattern & 0xFFFFFFFFu);
|
||||
unsigned high32 = (unsigned)(pattern >> 32);
|
||||
char token[96];
|
||||
|
||||
if (low32 != 0) {
|
||||
(void)snprintf(token, sizeof(token), "APP_ID=0x%08X", low32);
|
||||
append_shellcore_flag_token(dst, dst_size, token);
|
||||
}
|
||||
if (high32 != 0) {
|
||||
(void)snprintf(token, sizeof(token), "HIGH32=0x%08X", high32);
|
||||
append_shellcore_flag_token(dst, dst_size, token);
|
||||
}
|
||||
if (dst[0] == '\0')
|
||||
(void)snprintf(dst, dst_size, "0");
|
||||
return true;
|
||||
}
|
||||
|
||||
#if 0
|
||||
if (strcmp(flag->name, "SceShellCoreUtilUIStatus") == 0) {
|
||||
unsigned shape_field = (unsigned)((pattern >> 15) & 0x3u);
|
||||
if (shape_field != 0) {
|
||||
char token[96];
|
||||
(void)snprintf(token, sizeof(token), "BASIC_PRODUCT_SHAPE=%u",
|
||||
shape_field - 1u);
|
||||
append_shellcore_flag_token(dst, dst_size, token);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static void format_shellcore_flag_set_bits(const shellcore_flag_monitor_t *flag,
|
||||
uint64_t pattern, char *dst,
|
||||
size_t dst_size) {
|
||||
size_t bit_count = 0;
|
||||
uint64_t known_mask = 0;
|
||||
const shellcore_flag_bit_desc_t *bits = get_shellcore_flag_bits(flag, &bit_count);
|
||||
|
||||
if (!dst || dst_size == 0)
|
||||
return;
|
||||
dst[0] = '\0';
|
||||
|
||||
if (format_shellcore_flag_special_value(flag, pattern, dst, dst_size) &&
|
||||
dst[0] != '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < bit_count; ++i) {
|
||||
known_mask |= bits[i].mask;
|
||||
if ((pattern & bits[i].mask) == 0)
|
||||
continue;
|
||||
|
||||
char token[96];
|
||||
(void)snprintf(token, sizeof(token), "%s", bits[i].name);
|
||||
append_shellcore_flag_token(dst, dst_size, token);
|
||||
}
|
||||
|
||||
if (strcmp(flag->name, "SceShellCoreUtilAppFocus") == 0 &&
|
||||
(pattern >> 32) == 0 && pattern != 0) {
|
||||
char token[64];
|
||||
(void)snprintf(token, sizeof(token), "APP_ID=0x%08X", (unsigned)pattern);
|
||||
append_shellcore_flag_token(dst, dst_size, token);
|
||||
}
|
||||
|
||||
if ((pattern & ~known_mask) != 0) {
|
||||
char token[64];
|
||||
(void)snprintf(token, sizeof(token), "UNKNOWN=0x%llX",
|
||||
(unsigned long long)(pattern & ~known_mask));
|
||||
append_shellcore_flag_token(dst, dst_size, token);
|
||||
}
|
||||
|
||||
if (dst[0] == '\0')
|
||||
(void)snprintf(dst, dst_size, "0");
|
||||
}
|
||||
|
||||
static void set_shellcore_flag_start_result(bool success) {
|
||||
pthread_mutex_lock(&g_shellcore_flag_mutex);
|
||||
g_shellcore_flag_start_success = success;
|
||||
g_shellcore_flag_start_ready = true;
|
||||
pthread_cond_broadcast(&g_shellcore_flag_cond);
|
||||
pthread_mutex_unlock(&g_shellcore_flag_mutex);
|
||||
}
|
||||
|
||||
static void enter_sleep_mode_and_cleanup(const char *reason) {
|
||||
if (request_runtime_sleep_mode(true, reason)) {
|
||||
runtime_mount_state_lock();
|
||||
unmount_usb_sources_for_suspend();
|
||||
runtime_mount_state_unlock();
|
||||
}
|
||||
}
|
||||
|
||||
static void reset_shellcore_flag_state(shellcore_flag_monitor_t *flag) {
|
||||
if (!flag)
|
||||
return;
|
||||
|
||||
flag->handle = -1;
|
||||
flag->is_open = false;
|
||||
flag->has_last_pattern = false;
|
||||
flag->has_last_rc = false;
|
||||
flag->last_pattern = 0;
|
||||
flag->last_rc = 0;
|
||||
}
|
||||
|
||||
static void log_shellcore_flag_pattern(const shellcore_flag_monitor_t *flag,
|
||||
uint64_t pattern) {
|
||||
char decoded[512];
|
||||
|
||||
if (!flag)
|
||||
return;
|
||||
|
||||
format_shellcore_flag_set_bits(flag, pattern, decoded, sizeof(decoded));
|
||||
|
||||
if (!flag->has_last_pattern) {
|
||||
log_debug(" [SHELLFLAG] %s initial=0x%016llX low32=0x%08X high32=0x%08X "
|
||||
"decoded=%s",
|
||||
flag->name, (unsigned long long)pattern, (unsigned)(pattern),
|
||||
(unsigned)(pattern >> 32), decoded);
|
||||
return;
|
||||
}
|
||||
|
||||
log_debug(" [SHELLFLAG] %s value=0x%016llX delta=0x%016llX low32=0x%08X "
|
||||
"high32=0x%08X decoded=%s",
|
||||
flag->name, (unsigned long long)pattern,
|
||||
(unsigned long long)(flag->last_pattern ^ pattern),
|
||||
(unsigned)(pattern), (unsigned)(pattern >> 32), decoded);
|
||||
}
|
||||
|
||||
static void close_shellcore_flag(shellcore_flag_monitor_t *flag) {
|
||||
if (!flag || !flag->is_open)
|
||||
return;
|
||||
|
||||
int rc = sceKernelCloseEventFlag(flag->handle);
|
||||
if (rc < 0) {
|
||||
log_debug(" [SHELLFLAG] close failed: %s handle=0x%016llX rc=0x%08X",
|
||||
flag->name, (unsigned long long)flag->handle, (unsigned)rc);
|
||||
}
|
||||
|
||||
reset_shellcore_flag_state(flag);
|
||||
}
|
||||
|
||||
static void close_shellcore_flags(void) {
|
||||
for (size_t i = 0; i < sizeof(g_shellcore_flags) / sizeof(g_shellcore_flags[0]);
|
||||
++i) {
|
||||
close_shellcore_flag(&g_shellcore_flags[i]);
|
||||
}
|
||||
}
|
||||
|
||||
static bool open_shellcore_flags(size_t *opened_count_out) {
|
||||
size_t opened_count = 0;
|
||||
size_t required_count = 0;
|
||||
size_t required_opened = 0;
|
||||
|
||||
if (opened_count_out)
|
||||
*opened_count_out = 0;
|
||||
|
||||
for (size_t i = 0; i < sizeof(g_shellcore_flags) / sizeof(g_shellcore_flags[0]);
|
||||
++i) {
|
||||
shellcore_flag_monitor_t *flag = &g_shellcore_flags[i];
|
||||
sm_kernel_event_flag_t handle = -1;
|
||||
if (flag->required)
|
||||
required_count++;
|
||||
int rc = sceKernelOpenEventFlag(&handle, flag->name);
|
||||
if (rc < 0) {
|
||||
log_debug(" [SHELLFLAG] open failed: %s rc=0x%08X", flag->name,
|
||||
(unsigned)rc);
|
||||
reset_shellcore_flag_state(flag);
|
||||
continue;
|
||||
}
|
||||
|
||||
flag->handle = handle;
|
||||
flag->is_open = true;
|
||||
if (flag->required)
|
||||
required_opened++;
|
||||
log_debug(" [SHELLFLAG] opened %s handle=0x%016llX", flag->name,
|
||||
(unsigned long long)flag->handle);
|
||||
|
||||
{
|
||||
char known_masks[1024];
|
||||
format_shellcore_flag_known_masks(flag, known_masks, sizeof(known_masks));
|
||||
if (known_masks[0] != '\0') {
|
||||
log_debug(" [SHELLFLAG] known %s masks: %s", flag->name, known_masks);
|
||||
}
|
||||
}
|
||||
|
||||
opened_count++;
|
||||
}
|
||||
|
||||
if (opened_count_out)
|
||||
*opened_count_out = opened_count;
|
||||
return required_opened == required_count;
|
||||
}
|
||||
|
||||
static void poll_shellcore_flag(shellcore_flag_monitor_t *flag) {
|
||||
uint64_t result_pattern = 0;
|
||||
int rc;
|
||||
bool changed = false;
|
||||
bool entered_shutdown_on_going = false;
|
||||
bool entered_main_on_standby = false;
|
||||
bool entered_suspend_on_going = false;
|
||||
bool entered_resume_working = false;
|
||||
bool entered_shellui_shutdown_in_progress = false;
|
||||
bool is_system_state_mgr_info = false;
|
||||
unsigned current_state = 0;
|
||||
unsigned previous_state = 0;
|
||||
|
||||
if (!flag || !flag->is_open)
|
||||
return;
|
||||
|
||||
rc = sceKernelPollEventFlag(flag->handle, SHELLCORE_FLAG_ALL_BITS,
|
||||
SHELLCORE_FLAG_WAITMODE_OR, &result_pattern);
|
||||
if (rc < 0) {
|
||||
if (!flag->has_last_rc || flag->last_rc != rc) {
|
||||
log_debug(" [SHELLFLAG] %s poll rc=0x%08X", flag->name,
|
||||
(unsigned)rc);
|
||||
}
|
||||
flag->last_rc = rc;
|
||||
flag->has_last_rc = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!flag->has_last_rc || flag->last_rc != 0 || !flag->has_last_pattern ||
|
||||
flag->last_pattern != result_pattern) {
|
||||
log_shellcore_flag_pattern(flag, result_pattern);
|
||||
changed = flag->has_last_pattern && flag->last_pattern != result_pattern;
|
||||
}
|
||||
|
||||
is_system_state_mgr_info = strcmp(flag->name, "SceSystemStateMgrInfo") == 0;
|
||||
if (is_system_state_mgr_info) {
|
||||
current_state = (unsigned)(result_pattern & 0xFFFFu);
|
||||
previous_state =
|
||||
flag->has_last_pattern ? (unsigned)(flag->last_pattern & 0xFFFFu) : 0;
|
||||
|
||||
entered_shutdown_on_going =
|
||||
current_state == SYSTEM_STATE_MGR_STATE_SHUTDOWN_ON_GOING &&
|
||||
(!flag->has_last_pattern ||
|
||||
previous_state != SYSTEM_STATE_MGR_STATE_SHUTDOWN_ON_GOING);
|
||||
entered_main_on_standby =
|
||||
current_state == SYSTEM_STATE_MGR_STATE_MAIN_ON_STANDBY &&
|
||||
(!flag->has_last_pattern ||
|
||||
previous_state != SYSTEM_STATE_MGR_STATE_MAIN_ON_STANDBY);
|
||||
entered_suspend_on_going =
|
||||
current_state == SYSTEM_STATE_MGR_STATE_SUSPEND_ON_GOING &&
|
||||
(!flag->has_last_pattern ||
|
||||
previous_state != SYSTEM_STATE_MGR_STATE_SUSPEND_ON_GOING);
|
||||
entered_resume_working =
|
||||
flag->has_last_pattern &&
|
||||
current_state == SYSTEM_STATE_MGR_STATE_WORKING &&
|
||||
previous_state != SYSTEM_STATE_MGR_STATE_WORKING;
|
||||
} else if (strcmp(flag->name, "SceSystemStateMgrStatus") == 0) {
|
||||
entered_shellui_shutdown_in_progress =
|
||||
(result_pattern &
|
||||
SYSTEM_STATE_MGR_STATUS_SHELLUI_SHUTDOWN_IN_PROGRESS) != 0 &&
|
||||
(!flag->has_last_pattern ||
|
||||
(flag->last_pattern &
|
||||
SYSTEM_STATE_MGR_STATUS_SHELLUI_SHUTDOWN_IN_PROGRESS) == 0);
|
||||
}
|
||||
|
||||
flag->last_pattern = result_pattern;
|
||||
flag->has_last_pattern = true;
|
||||
flag->last_rc = 0;
|
||||
flag->has_last_rc = true;
|
||||
|
||||
if (changed && strcmp(flag->name, "SceShellCoreUtilAppFocus") == 0) {
|
||||
sm_kstuff_note_app_focus((uint32_t)result_pattern);
|
||||
wake_game_lifecycle_watcher();
|
||||
}
|
||||
if (entered_shutdown_on_going) {
|
||||
request_shutdown_stop("SceSystemStateMgrInfo=SHUTDOWN_ON_GOING");
|
||||
}
|
||||
if (entered_main_on_standby || entered_suspend_on_going) {
|
||||
const char *sleep_reason =
|
||||
entered_main_on_standby ? "SceSystemStateMgrInfo=MAIN_ON_STANDBY"
|
||||
: "SceSystemStateMgrInfo=SUSPEND_ON_GOING";
|
||||
enter_sleep_mode_and_cleanup(sleep_reason);
|
||||
}
|
||||
if (entered_shellui_shutdown_in_progress) {
|
||||
enter_sleep_mode_and_cleanup(
|
||||
"SceSystemStateMgrStatus=SHELLUI_SHUTDOWN_IN_PROGRESS");
|
||||
}
|
||||
if (entered_resume_working) {
|
||||
request_scan_now("SceSystemStateMgrInfo=WORKING");
|
||||
request_runtime_sleep_mode(false, "SceSystemStateMgrInfo=WORKING");
|
||||
}
|
||||
}
|
||||
|
||||
static void *shellcore_flag_thread_main(void *arg) {
|
||||
(void)arg;
|
||||
|
||||
size_t opened_count = 0;
|
||||
if (!open_shellcore_flags(&opened_count)) {
|
||||
if (opened_count != 0)
|
||||
close_shellcore_flags();
|
||||
set_shellcore_flag_start_result(false);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < sizeof(g_shellcore_flags) / sizeof(g_shellcore_flags[0]);
|
||||
++i) {
|
||||
poll_shellcore_flag(&g_shellcore_flags[i]);
|
||||
}
|
||||
|
||||
log_debug(" [SHELLFLAG] monitor started (%zu/%zu flags opened)", opened_count,
|
||||
sizeof(g_shellcore_flags) / sizeof(g_shellcore_flags[0]));
|
||||
set_shellcore_flag_start_result(true);
|
||||
|
||||
while (!g_shellcore_flag_stop_requested && !should_stop_requested()) {
|
||||
for (size_t i = 0; i < sizeof(g_shellcore_flags) / sizeof(g_shellcore_flags[0]);
|
||||
++i) {
|
||||
poll_shellcore_flag(&g_shellcore_flags[i]);
|
||||
}
|
||||
|
||||
sceKernelUsleep(SHELLCORE_FLAG_POLL_INTERVAL_US);
|
||||
}
|
||||
|
||||
close_shellcore_flags();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool sm_shellcore_flags_start(void) {
|
||||
int rc;
|
||||
|
||||
if (g_shellcore_flag_thread_started)
|
||||
return g_shellcore_flag_start_success;
|
||||
|
||||
g_shellcore_flag_stop_requested = 0;
|
||||
g_shellcore_flag_start_ready = false;
|
||||
g_shellcore_flag_start_success = false;
|
||||
close_shellcore_flags();
|
||||
|
||||
rc = pthread_create(&g_shellcore_flag_thread, NULL, shellcore_flag_thread_main,
|
||||
NULL);
|
||||
if (rc != 0) {
|
||||
log_debug(" [SHELLFLAG] pthread_create failed: rc=%d", rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_shellcore_flag_mutex);
|
||||
while (!g_shellcore_flag_start_ready)
|
||||
pthread_cond_wait(&g_shellcore_flag_cond, &g_shellcore_flag_mutex);
|
||||
pthread_mutex_unlock(&g_shellcore_flag_mutex);
|
||||
|
||||
if (!g_shellcore_flag_start_success) {
|
||||
pthread_join(g_shellcore_flag_thread, NULL);
|
||||
close_shellcore_flags();
|
||||
return false;
|
||||
}
|
||||
|
||||
g_shellcore_flag_thread_started = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void sm_shellcore_flags_stop(void) {
|
||||
if (!g_shellcore_flag_thread_started)
|
||||
return;
|
||||
|
||||
g_shellcore_flag_stop_requested = 1;
|
||||
pthread_join(g_shellcore_flag_thread, NULL);
|
||||
g_shellcore_flag_thread_started = false;
|
||||
g_shellcore_flag_start_ready = false;
|
||||
g_shellcore_flag_start_success = false;
|
||||
close_shellcore_flags();
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#include "sm_platform.h"
|
||||
#include "sm_stability.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_types.h"
|
||||
|
||||
static time_t get_path_last_change_time(const struct stat *st) {
|
||||
if (!st)
|
||||
return 0;
|
||||
return (st->st_ctime > st->st_mtime) ? st->st_ctime : st->st_mtime;
|
||||
}
|
||||
|
||||
bool is_path_stable_now(const char *path, double *root_diff_out,
|
||||
int *stat_errno_out) {
|
||||
struct stat st;
|
||||
time_t now = time(NULL);
|
||||
ensure_runtime_config_ready();
|
||||
if (stat_errno_out)
|
||||
*stat_errno_out = 0;
|
||||
|
||||
if (stat(path, &st) != 0) {
|
||||
if (root_diff_out)
|
||||
*root_diff_out = -1.0;
|
||||
if (stat_errno_out)
|
||||
*stat_errno_out = errno;
|
||||
return false;
|
||||
}
|
||||
|
||||
double root_diff = difftime(now, get_path_last_change_time(&st));
|
||||
if (root_diff_out)
|
||||
*root_diff_out = root_diff;
|
||||
if (root_diff < 0.0)
|
||||
return true;
|
||||
return root_diff > (double)runtime_config()->stability_wait_seconds;
|
||||
}
|
||||
|
||||
bool wait_for_stability_fast(const char *path, const char *name) {
|
||||
double diff = 0.0;
|
||||
int st_err = 0;
|
||||
if (is_path_stable_now(path, &diff, &st_err))
|
||||
return true;
|
||||
|
||||
if (st_err != 0)
|
||||
log_debug(" [WAIT] %s stat failed for %s: %s", name, path,
|
||||
strerror(st_err));
|
||||
else
|
||||
log_debug(" [WAIT] %s modified %.0fs ago. Waiting...", name, diff);
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include "sm_time.h"
|
||||
|
||||
uint64_t monotonic_time_us(void) {
|
||||
struct timespec ts;
|
||||
memset(&ts, 0, sizeof(ts));
|
||||
if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0)
|
||||
return 0;
|
||||
|
||||
return (uint64_t)ts.tv_sec * 1000000ull + (uint64_t)ts.tv_nsec / 1000ull;
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
#include "sm_platform.h"
|
||||
#include "sm_title_state.h"
|
||||
#include "sm_hash.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
|
||||
struct TitleStateEntry {
|
||||
char title_id[MAX_TITLE_ID];
|
||||
uint8_t mount_reg_attempts;
|
||||
uint8_t register_attempts;
|
||||
bool duplicate_notified_once;
|
||||
bool valid;
|
||||
};
|
||||
|
||||
static struct TitleStateEntry g_title_state[TITLE_STATE_CAPACITY];
|
||||
static uint16_t g_title_state_hash[STATE_HASH_SIZE];
|
||||
|
||||
static void rebuild_title_state_hash(void) {
|
||||
memset(g_title_state_hash, 0, sizeof(g_title_state_hash));
|
||||
for (int k = 0; k < TITLE_STATE_CAPACITY; k++) {
|
||||
if (!g_title_state[k].valid || g_title_state[k].title_id[0] == '\0')
|
||||
continue;
|
||||
uint32_t slot =
|
||||
sm_fnv1a32(g_title_state[k].title_id) & (STATE_HASH_SIZE - 1u);
|
||||
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
|
||||
if (g_title_state_hash[slot] == 0) {
|
||||
g_title_state_hash[slot] = (uint16_t)(k + 1);
|
||||
break;
|
||||
}
|
||||
slot = (slot + 1u) & (STATE_HASH_SIZE - 1u);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static struct TitleStateEntry *find_title_state(const char *title_id) {
|
||||
uint32_t slot = sm_fnv1a32(title_id) & (STATE_HASH_SIZE - 1u);
|
||||
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
|
||||
uint16_t idx = g_title_state_hash[slot];
|
||||
if (idx == 0)
|
||||
return NULL;
|
||||
struct TitleStateEntry *entry = &g_title_state[idx - 1u];
|
||||
if (entry->valid && strcmp(entry->title_id, title_id) == 0)
|
||||
return entry;
|
||||
slot = (slot + 1u) & (STATE_HASH_SIZE - 1u);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static struct TitleStateEntry *create_title_state(const char *title_id) {
|
||||
int slot_k = -1;
|
||||
for (int k = 0; k < TITLE_STATE_CAPACITY; k++) {
|
||||
if (!g_title_state[k].valid) {
|
||||
slot_k = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (slot_k < 0) {
|
||||
int evict_k = -1;
|
||||
for (int k = 0; k < TITLE_STATE_CAPACITY; k++) {
|
||||
if (!g_title_state[k].valid)
|
||||
continue;
|
||||
if (g_title_state[k].mount_reg_attempts == 0 &&
|
||||
g_title_state[k].register_attempts == 0) {
|
||||
evict_k = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (evict_k < 0) {
|
||||
for (int k = 0; k < TITLE_STATE_CAPACITY; k++) {
|
||||
if (!g_title_state[k].valid)
|
||||
continue;
|
||||
if (g_title_state[k].mount_reg_attempts == 0) {
|
||||
evict_k = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (evict_k < 0)
|
||||
evict_k = 0;
|
||||
memset(&g_title_state[evict_k], 0, sizeof(g_title_state[evict_k]));
|
||||
rebuild_title_state_hash();
|
||||
slot_k = evict_k;
|
||||
}
|
||||
|
||||
memset(&g_title_state[slot_k], 0, sizeof(g_title_state[slot_k]));
|
||||
g_title_state[slot_k].valid = true;
|
||||
(void)strlcpy(g_title_state[slot_k].title_id, title_id,
|
||||
sizeof(g_title_state[slot_k].title_id));
|
||||
|
||||
uint32_t slot = sm_fnv1a32(title_id) & (STATE_HASH_SIZE - 1u);
|
||||
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
|
||||
if (g_title_state_hash[slot] == 0) {
|
||||
g_title_state_hash[slot] = (uint16_t)(slot_k + 1);
|
||||
return &g_title_state[slot_k];
|
||||
}
|
||||
slot = (slot + 1u) & (STATE_HASH_SIZE - 1u);
|
||||
}
|
||||
|
||||
g_title_state[slot_k].valid = false;
|
||||
g_title_state[slot_k].title_id[0] = '\0';
|
||||
rebuild_title_state_hash();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static struct TitleStateEntry *get_or_create_title_state(const char *title_id) {
|
||||
struct TitleStateEntry *entry = find_title_state(title_id);
|
||||
return entry ? entry : create_title_state(title_id);
|
||||
}
|
||||
|
||||
bool was_register_attempted(const char *title_id) {
|
||||
struct TitleStateEntry *entry = find_title_state(title_id);
|
||||
return entry ? (entry->register_attempts >= MAX_REGISTER_ATTEMPTS) : false;
|
||||
}
|
||||
|
||||
uint8_t get_register_attempts(const char *title_id) {
|
||||
struct TitleStateEntry *entry = find_title_state(title_id);
|
||||
return entry ? entry->register_attempts : 0;
|
||||
}
|
||||
|
||||
void mark_register_attempted(const char *title_id) {
|
||||
struct TitleStateEntry *entry = get_or_create_title_state(title_id);
|
||||
if (entry->register_attempts < UINT8_MAX)
|
||||
entry->register_attempts++;
|
||||
}
|
||||
|
||||
void clear_register_attempts(const char *title_id) {
|
||||
struct TitleStateEntry *entry = find_title_state(title_id);
|
||||
if (entry)
|
||||
entry->register_attempts = 0;
|
||||
}
|
||||
|
||||
void notify_duplicate_title_once(const char *title_id, const char *path_a,
|
||||
const char *path_b) {
|
||||
struct TitleStateEntry *entry = get_or_create_title_state(title_id);
|
||||
if (entry->duplicate_notified_once)
|
||||
return;
|
||||
entry->duplicate_notified_once = true;
|
||||
notify_system("Duplicate %s ignored:\n%s\nexisting: %s", title_id, path_a,
|
||||
path_b);
|
||||
}
|
||||
|
||||
void clear_duplicate_title_notification(const char *title_id) {
|
||||
struct TitleStateEntry *entry = find_title_state(title_id);
|
||||
if (entry)
|
||||
entry->duplicate_notified_once = false;
|
||||
}
|
||||
|
||||
uint8_t get_failed_mount_attempts(const char *title_id) {
|
||||
struct TitleStateEntry *entry = find_title_state(title_id);
|
||||
return entry ? entry->mount_reg_attempts : 0;
|
||||
}
|
||||
|
||||
void clear_failed_mount_attempts(const char *title_id) {
|
||||
struct TitleStateEntry *entry = find_title_state(title_id);
|
||||
if (entry)
|
||||
entry->mount_reg_attempts = 0;
|
||||
}
|
||||
|
||||
uint8_t bump_failed_mount_attempts(const char *title_id) {
|
||||
struct TitleStateEntry *entry = get_or_create_title_state(title_id);
|
||||
if (entry->mount_reg_attempts < UINT8_MAX)
|
||||
entry->mount_reg_attempts++;
|
||||
return entry->mount_reg_attempts;
|
||||
}
|
||||
Reference in new issue
Block a user