Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/build-be.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ jobs:
New-Item -ItemType Directory -Force -Path ./artifacts/release/x86
New-Item -ItemType Directory -Force -Path ./artifacts/verbose/x64
New-Item -ItemType Directory -Force -Path ./artifacts/verbose/x86
- name: Run Windows regression tests
run: ./tests/windows/run-regressions.ps1
- name: Build Release
Comment on lines +18 to 20
run: |
./build.ps1
Expand Down Expand Up @@ -73,6 +75,8 @@ jobs:
cp assets/nix/run.sh artifacts/release/x86/run.sh
cp assets/nix/run.sh artifacts/release/x64/run.sh
cp LICENSE artifacts/release/LICENSE
- name: Run nix regression tests
run: sh tests/nix/run-regressions.sh build/linux/x64/release/libdoorstop.so
- name: Build Verbose
run: |
./build.sh -with_logging
Expand Down Expand Up @@ -128,6 +132,8 @@ jobs:
cp build/macosx/universal/release/.doorstop_version artifacts/release/universal/.doorstop_version
cp assets/nix/run.sh artifacts/release/universal/run.sh
cp LICENSE artifacts/release/LICENSE
- name: Run macOS interposition smoke test
run: sh tests/nix/run-macos-interpose-smoke.sh build/macosx/universal/release/libdoorstop.dylib
- name: Build Verbose
run: |
./build.sh -with_logging
Expand Down
89 changes: 86 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,11 @@ Doorstop sets some environment variables useful for code execution:

| Environment variable | Description |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `DOORSTOP_INITIALIZED` | Always set to `TRUE`. Use to determine if your code is run via Doorstop. |
| `DOORSTOP_INVOKE_DLL_PATH` | Path to the assembly executed by Doorstop relative to the current working directory. |
| `DOORSTOP_INITIALIZED` | Set to `TRUE` when Doorstop bootstraps a managed target. Use to determine if your code is run via Doorstop. |
| `DOORSTOP_INVOKE_DLL_PATH` | Full path to the assembly executed by Doorstop. |
| `DOORSTOP_PROCESS_PATH` | Path to the application executable where the injected assembly is run. |
| `DOORSTOP_MANAGED_FOLDER_DIR` | *UnityMono*: Path to the game's `Managed` folder. *Il2Cpp*: Path to CoreCLR's base class library folder. |
| `DOORSTOP_DLL_SEARCH_DIRS` | Paths where the runtime searchs assemblies from by default, separated by OS-specific separator (`;` on windows and `:` on *nix). |
| `DOORSTOP_DLL_SEARCH_DIRS` | Paths where the runtime searches for assemblies by default, separated by the OS-specific separator (`;` on Windows and `:` on Unix). |
| `DOORSTOP_MONO_LIB_PATH` | *Only on UnityMono*: Full path to the mono runtime library. |

### Debugging
Expand Down Expand Up @@ -142,6 +142,89 @@ All Doorstop arguments start with `--doorstop-` and always contain an argument.
| `--doorstop-clr-corlib-dir string` | Path to coreclr library that contains the CoreCLR runtime |
| `--doorstop-clr-runtime-coreclr-path string` | Path to the directory containing the managed core libraries for CoreCLR (`mscorlib`, `System`, etc.) |

## Troubleshooting and compatibility

### Choosing a Windows proxy DLL

Windows builds can be installed as `winhttp.dll`, `version.dll`, or
`dxgi.dll`. If a game or launcher uses WinHTTP during very early startup,
proxying `winhttp.dll` can conflict with that initialization. Rename the
Doorstop proxy to `dxgi.dll` (or `version.dll`) and keep
`doorstop_config.ini` beside it. This is particularly useful for games that
silently exit before Doorstop reaches the managed bootstrap.
Install only one of these proxy DLL names at a time.

### Native Unix games, Proton, and relative paths

`run.sh` is for native Linux and macOS executables. A Windows PE executable
running through Wine or Proton must use the Windows Doorstop build and a
Windows proxy DLL instead.

Relative executable and `target_assembly` paths in `run.sh` are resolved from
the script directory, so the script can be launched from another working
directory. Each relative entry in the `dll_search_path_override` list is
resolved from that directory as well. The target assembly's parent directory
is not added to Mono's search path automatically. Separate multiple paths with
`;` on Windows and `:` on Unix.

### Debug-only mode

Mono debugging can be enabled without loading a target assembly. Set
`debug_enabled=true` (or `debug_enable=1` in `run.sh`) and leave
`target_assembly` empty. Doorstop installs the Mono initialization hook and
configures the debugger, then skips the managed entrypoint.

Doorstop accepts `localhost` for the Mono debugger and canonicalizes it to
`127.0.0.1`; using `127.0.0.1` in the IDE avoids an IPv6 (`::1`) DNS choice on
clients that do not retry IPv4. An explicitly configured IPv6 listener remains
distinct. Doorstop also honors dnSpy's `DNSPY_UNITY_DBG2` environment variable,
so clear a stale value when debugging is unexpectedly enabled.

On modern macOS Mach-O images that use chained fixups, Doorstop uses dyld
interposition for runtime symbol lookup, boot.config access, and UnityPlayer's
stdout protection. Those stdio hooks verify that their caller is UnityPlayer,
so an inherited `DYLD_INSERT_LIBRARIES` value does not change shell or launcher
redirection. Legacy Mono players that bind initialization directly continue to
use the traditional Mach-O lazy-bind path.

Reconnect behavior after an IDE disconnect is implemented by the Mono runtime
bundled with the game. Some older Unity Mono versions do not reliably reopen
their listener; Doorstop cannot replace that runtime-side connection loop.

### Games that restart themselves

Steam and self-restarting games can copy `DOORSTOP_DISABLE` and
`DOORSTOP_INITIALIZED` into the replacement process. Set
`ignore_disable_switch=true` on Windows, or `ignore_disable_switch=1` in
`run.sh`, when the launcher is known to do this. Doorstop then clears both
inherited markers; the Mono bootstrap also uses a process-local guard to
prevent genuine duplicate initialization.

### Waiting until a game assembly is loaded

Doorstop deliberately invokes `Doorstop.Entrypoint.Start()` before game
assemblies. A target that needs types from `Assembly-CSharp` can use the
managed assembly-load event instead of a version-specific native hook:

```cs
AppDomain.CurrentDomain.AssemblyLoad += (_, eventArgs) =>
{
if (eventArgs.LoadedAssembly.GetName().Name == "Assembly-CSharp")
OnGameAssemblyLoaded();
};
```

Register the handler in `Start()` and make the callback one-shot if the loader
must run only once.

### RenderDoc and other native hook tools

Doorstop preserves and calls through a pre-existing `GetProcAddress` IAT hook,
which allows common RenderDoc/apitrace injection orders to coexist. Tools that
replace other required import-table entries can still conflict; in that case,
start through Doorstop first, load or attach the graphics tool from the managed
entrypoint, and then capture the running process.


## License

Expand Down
77 changes: 65 additions & 12 deletions assets/nix/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ target_assembly="Doorstop.dll"
# Overrides the default boot.config file path
boot_config_override=

# If enabled, DOORSTOP_DISABLE env var value is ignored
# If enabled, inherited DOORSTOP_DISABLE and DOORSTOP_INITIALIZED markers are
# ignored so launchers can safely start a replacement game process
# USE THIS ONLY WHEN ASKED TO OR YOU KNOW WHAT THIS MEANS
ignore_disable_switch="0"

Expand Down Expand Up @@ -62,9 +63,20 @@ corlib_dir=""
# Everything past this point is the actual script
set -e

# Use POSIX-compatible way to get the directory of the script. Relative game
# paths are resolved from here, not from the caller's working directory.
a="/$0"; a=${a%/*}; a=${a#/}; a=${a:-.}; BASEDIR=$(cd "$a" || exit; pwd -P)

script_path() {
case "$1" in
/*) printf '%s\n' "$1" ;;
*) printf '%s\n' "${BASEDIR}/$1" ;;
esac
}

# Special case: program is launched via Steam on Linux
# In that case rerun the script via their bootstrapper to delay adding Doorstop to LD_PRELOAD
# This is required until https://github.com/NeighTools/UnityDoorstop/issues/88 is resolved
# and avoid injecting Doorstop into the bootstrapper and overlay helpers.
for a in "$@"; do
if [ "$a" = "SteamLaunch" ]; then
rotated=0; max=$#
Expand All @@ -90,18 +102,20 @@ for a in "$@"; do
done

# Handle first param being executable name
if [ -x "$1" ] ; then
executable_name="$1"
shift
if [ -n "$1" ]; then
first_arg_path="$(script_path "$1")"
if [ -x "$first_arg_path" ] ; then
executable_name="$1"
shift
fi
fi

if [ -z "${executable_name}" ] || [ ! -x "${executable_name}" ]; then
executable_path_from_base="$(script_path "$executable_name")"
if [ -z "${executable_name}" ] || [ ! -x "${executable_path_from_base}" ]; then
echo "Please set executable_name to a valid name in a text editor or as the first command line parameter" 1>&2
exit 1
fi

# Use POSIX-compatible way to get the directory of the executable
a="/$0"; a=${a%/*}; a=${a#/}; a=${a:-.}; BASEDIR=$(cd "$a" || exit; pwd -P)
executable_name="${executable_path_from_base}"

arch=""
executable_path=""
Expand All @@ -115,6 +129,38 @@ abs_path() {
echo "$(cd "$(dirname "$1")" && pwd)/$(basename "$1")"
}

normalize_search_paths() {
remaining_paths="$1"
normalized_paths=""

while :; do
case "$remaining_paths" in
*:*)
search_path=${remaining_paths%%:*}
remaining_paths=${remaining_paths#*:}
has_more_paths=1
;;
*)
search_path=$remaining_paths
has_more_paths=0
;;
esac

if [ -n "$search_path" ]; then
search_path="$(abs_path "$search_path")"
if [ -n "$normalized_paths" ]; then
normalized_paths="${normalized_paths}:${search_path}"
else
normalized_paths=$search_path
fi
fi

[ "$has_more_paths" -eq 1 ] || break
done

printf '%s\n' "$normalized_paths"
}

# Set executable path and the extension to use for the libdoorstop shared object as well as check whether we're running on Apple Silicon
os_type="$(uname -s)"
case ${os_type} in
Expand Down Expand Up @@ -288,7 +334,10 @@ while [ $i -lt $max ]; do
i=$((i+1))
done

target_assembly="$(abs_path "$target_assembly")"
if [ -n "$target_assembly" ]; then
target_assembly="$(abs_path "$target_assembly")"
fi
dll_search_path_override="$(normalize_search_paths "$dll_search_path_override")"

# Move variables to environment
export DOORSTOP_ENABLED="$enabled"
Expand Down Expand Up @@ -326,8 +375,12 @@ if [ -n "${is_apple_silicon}" ]; then
# We need to use arch for Apple Silicon to allow the executable to be run natively as otherwise if
# the executable is universal, supporting both x86_64 and arm64, MacOs will still run it as x86_64
# if the parent process is running as x86.
# arch also strips the DYLD_INSERT_LIBRARIES env var so we have to pass that in manually
exec arch -e DYLD_INSERT_LIBRARIES="${DYLD_INSERT_LIBRARIES}" "$executable_path" "$@"
# Keep the inserted library out of the arm64e arch helper itself, then add
# it back only for the game process. This must use the shell builtin unset;
# an external env helper would encounter the same architecture mismatch.
doorstop_insert="${DYLD_INSERT_LIBRARIES}"
unset DYLD_INSERT_LIBRARIES
exec arch -e DYLD_INSERT_LIBRARIES="${doorstop_insert}" "$executable_path" "$@"
else
exec "$executable_path" "$@"
fi
2 changes: 1 addition & 1 deletion assets/windows/doorstop_config.ini
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ redirect_output_log=false
# Overrides the default boot.config file path
boot_config_override=

# If enabled, DOORSTOP_DISABLE env var value is ignored
# If enabled, inherited DOORSTOP_DISABLE and DOORSTOP_INITIALIZED values are ignored
# USE THIS ONLY WHEN ASKED TO OR YOU KNOW WHAT THIS MEANS
ignore_disable_switch=false

Expand Down
Loading
Loading