Skip to content
Draft
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
23 changes: 23 additions & 0 deletions tests/xharness/Harness.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,29 @@ string GetVariable (string variable, string @default = null)
return result;
}

#nullable enable
string? spawnerPath;
public string SpawnerPath {
get {
if (spawnerPath is null)
spawnerPath = Path.GetFullPath (Path.Combine (RootDirectory, "..", "tools", "spawner", "spawner"));
return spawnerPath;
}
}

public void UseSpawner (ProcessStartInfo processStartInfo, IList<string> arguments)
{
if (!string.IsNullOrEmpty (processStartInfo.Arguments))
throw new InvalidOperationException ($"ProcessStartInfo.Arguments must be empty when using UseSpawner.");

var originalFileName = processStartInfo.FileName;
processStartInfo.FileName = SpawnerPath;
processStartInfo.ArgumentList.Add (originalFileName);
foreach (var args in arguments)
processStartInfo.ArgumentList.Add (args);
}
#nullable disable

public List<TestProject> TestProjects { get; } = new ();

readonly bool useSystemXamarinIOSMac; // if the system XI/XM should be used, or the locally build XI/XM.
Expand Down
2 changes: 2 additions & 0 deletions tests/xharness/IHarness.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ public interface IHarness {
bool InCI { get; }
bool UseTcpTunnel { get; }
string VSDropsUri { get; }
string SpawnerPath { get; }
void UseSpawner (System.Diagnostics.ProcessStartInfo startInfo, IList<string> arguments);

#endregion

Expand Down
5 changes: 4 additions & 1 deletion tests/xharness/Jenkins/TestTasks/MacExecuteTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,10 @@ public override async Task RunTestAsync ()
proc.StartInfo.EnvironmentVariables ["DISABLE_SYSTEM_PERMISSION_TESTS"] = "1";
proc.StartInfo.EnvironmentVariables ["MONO_DEBUG"] = "no-gdb-backtrace";
proc.StartInfo.EnvironmentVariables.Remove ("DYLD_FALLBACK_LIBRARY_PATH"); // VSMac might set this, and the test may end up crashing
proc.StartInfo.Arguments = StringUtils.FormatArguments (arguments);

// Use the spawner to launch the app, to avoid issues with macOS getting confused who's the responsible process
Harness.UseSpawner (proc.StartInfo, arguments);

Jenkins.MainLog.WriteLine ("Executing {0} ({1})", TestName, Mode);
var log = Logs.Create ($"execute-{Platform}-{Timestamp}.txt", LogType.ExecutionLog.ToString ());
ICrashSnapshotReporter snapshot = null;
Expand Down
1 change: 1 addition & 0 deletions tools/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ endif
SUBDIRS+=mlaunch

SUBDIRS += dotnet-linker
SUBDIRS += spawner
3 changes: 2 additions & 1 deletion tools/devops/automation/templates/tests/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,9 @@ steps:
make -C src build/generator-frameworks.g.cs
make -C src build/ios/Constants.cs
make -C msbuild Versions.g.cs
make -C tools/spawner
workingDirectory: $(System.DefaultWorkingDirectory)/$(BUILD_REPOSITORY_TITLE)
displayName: Generate constants files
displayName: "Generate / compile dependencies"
timeoutInMinutes: 15

- pwsh: >-
Expand Down
3 changes: 3 additions & 0 deletions tools/spawner/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.libs
spawner

15 changes: 15 additions & 0 deletions tools/spawner/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
TOP=../..
include $(TOP)/Make.config
include $(TOP)/mk/rules.mk

.libs/osx-arm64/spawner: .libs/osx-arm64/spawner.o
$(Q_CCLD) $(CLANG) -arch $(DOTNET_osx-arm64_ARCHITECTURES) $< -o$@ -isysroot $(macos_SDK)

.libs/osx-x64/spawner: .libs/osx-x64/spawner.o
$(Q_CCLD) $(CLANG) -arch $(DOTNET_osx-x64_ARCHITECTURES) $< -o$@ -isysroot $(macos_SDK)

spawner: .libs/osx-arm64/spawner .libs/osx-x64/spawner
$(Q_LIPO) $(LIPO) $^ -create -output $@
$(Q) chmod +x $@

all-local:: spawner
69 changes: 69 additions & 0 deletions tools/spawner/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
SPAWNER
=======

This is a very simple tool, which executes another process, disclaiming any
responsibility for it.

This is important when executing tests apps, because when macOS sees that an
app uses API that needs specific entries in the Info.plist (such as the
`NSAppleMusicUsageDescription`), the responsible process is where macOS looks
for said key.

Example crash report:

```
Process: introspection [85822]
Path: /Users/USER/*/introspection.app/Contents/MacOS/introspection
Identifier: com.xamarin.introspection
Version: 1.0 (1.0)
Code Type: ARM-64 (Native)
Parent Process: dotnet [81129]
Responsible: Electron [68966]
User ID: 501

Date/Time: 2025-11-13 17:33:10.8123 +0100
OS Version: macOS 15.7.2 (24G325)
Report Version: 12
Anonymous UUID: F22C0F06-0F16-E475-C0CB-264A0FF4F6A3


Time Awake Since Boot: 27000 seconds

System Integrity Protection: enabled

Crashed Thread: 16 Dispatch queue: com.apple.root.default-qos

Exception Type: EXC_CRASH (SIGKILL)
Exception Codes: 0x0000000000000000, 0x0000000000000000

Termination Reason: Namespace TCC, Code 0
This app has crashed because it attempted to access privacy-sensitive data without a usage description. The app's Info.plist must contain an NSAppleMusicUsageDescription key with a string value explaining to the user how the app uses this data.
```

The app crashed because macOS says it needs the `NSAppleMusicUsageDescription` entry in its `Info.plist` file.

This is confusing, because introspection has an `NSAppleMusicUsageDescription` entry in its `Info.plist` file.

Here's what happens:

Note that there's a "Responsible [Process]" (Electron 68966) line, which is not the same as "Process" (introspection 85822), and this is the crux of the matter.

In this particular case:

* I opened the xharness project in VSCode.
* I launched the xharness project in the debugger, and then ran introspection for Mac Catalyst.
* The responsible process ended up being VS Code (aka Electron, with pid 85822), and that's where macOS ended up looking for the `NSAppleMusicUsageDescription` key.

The fix is to launch `introspection` (and any other test app on macOS) using
this `spawner` tool, which disclaims reponsibility for anything it launches,
thus letting `introspection` be a grown up process and fully responsible for
itself.

Usage is simple: just pass the executable + any arguments to `spawner`.

References:

* https://gitlab.com/gnachman/iterm2/-/issues/10360
* https://github.com/llvm/llvm-project/commit/041c7b84a4b925476d1e21ed302786033bb6035f#diff-a38ae411ccf0c85f3d7c0c45d8e1ad035030d5171d59e478b86a094941d3209dR16-R17
* https://lldb.llvm.org/cpp_reference/PosixSpawnResponsible_8h_source.html
* https://steipete.me/posts/2025/applescript-cli-macos-complete-guide
71 changes: 71 additions & 0 deletions tools/spawner/spawner.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#include <dispatch/dispatch.h>
#include <dlfcn.h>
#include <signal.h>
#include <spawn.h>
#include <stdio.h>

errno_t responsibility_spawnattrs_setdisclaim (posix_spawnattr_t *attrs, bool disclaim);

int main (int argc, char** argv, char** envp)
{
if (argc < 2) {
fprintf (stderr,
"spawner: launch a subprocess, disclaiming all responsibilities with regards to TCC:\n"
"usage: spawner <command> [arguments]\n");
return 1;
}

int rv;
// Behave as exec
short flags = POSIX_SPAWN_SETEXEC;
posix_spawnattr_t spawnattr;
sigset_t sigset;

rv = posix_spawnattr_init (&spawnattr);
if (rv) {
fprintf (stderr, "Failed to execute 'posix_spawnattr_init': %i (%s)\n", rv, strerror (rv));
return 1;
}

// Reset the signal mask
sigemptyset (&sigset);
rv = posix_spawnattr_setsigmask (&spawnattr, &sigset);
if (rv) {
fprintf (stderr, "Failed to execute 'posix_spawnattr_setsigmask': %i (%s)\n", rv, strerror (rv));
return 1;
}
flags |= POSIX_SPAWN_SETSIGMASK;

// Reset all signals to their default handlers
sigfillset (&sigset);
rv = posix_spawnattr_setsigdefault (&spawnattr, &sigset);
if (rv) {
fprintf (stderr, "Failed to execute 'posix_spawnattr_setsigdefault': %i (%s)\n", rv, strerror (rv));
return 1;
}
flags |= POSIX_SPAWN_SETSIGDEF;

rv = posix_spawnattr_setflags (&spawnattr, flags);
if (rv) {
fprintf (stderr, "Failed to execute 'posix_spawnattr_setflags': %i (%s)\n", rv, strerror (rv));
return 1;
}

rv = responsibility_spawnattrs_setdisclaim (&spawnattr, 1);
if (rv) {
fprintf (stderr, "Failed to execute 'responsibility_spawnattrs_setdisclaim': %i (%s)\n", rv, strerror (rv));
return 1;
}

pid_t pid = 0;
rv = posix_spawnp (&pid, argv [1], NULL, &spawnattr, argv + 1, envp);
posix_spawnattr_destroy (&spawnattr);

// posix_spawnp shouldn't return (because we set the POSIX_SPAWN_SETEXEC flag)
// so if it did, something went wrong
fprintf (stderr, "Failed to execute '%s': %i (%s)\n", argv [1], rv, strerror (rv));
return 1;
}
Loading