Swift Actor Hanging - Control over execution not returned after async call

I am experiencing an issue with my actor when I am trying to call another async function that is static and declared to run on the main-actor. Specifically, the called function executes to the end, but never seems to return control over execution to the caller (my actor).

This results in the code after the function call to never be executed.

In simple terms, it looks like the executing MainThread simply "swallows" the return statement.

Below is a minimal example of what my code (structurally) looks like. What actually happens in the called function are ScreenCaptureKit calls. I decided to leave them in for clarity.

actor SomeActor {
  func myTestFunction() async throws {
    print("Before execution")
    let returnValue = try await SomeClass.createSCKSnapshot()
    print("After execution") // this is never called since the previous await never seems to return execution back to SomeActor
  }
}

@MainActor
class SomeClass {
  @MainActor public static func createSCKSnapshot() async throws -> (displays: [SCDisplay], windows: [SCWindow], applications: [SCRunningApplication]) {
    let shareableContent: SCShareableContent = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: false)

    return (shareableContent.displays, shareableContent.windows, shareableContent.applications)
  }
}

Now, here you see my actor SomeActor trying to call SomeClass.createSCKSnapshot() in one of its isolated functions. Thereby, createSCKSnapshot() executes just fine including the return statement, but then nothing happens and the print("After execution") is never executed inside SomeActor.

Why is that? And how can I bypass this issue and ensure that my program continues execution normally after createSCKSnapshot() returns?

NOTE:

The app does have permission and uses ScreenCaptureKit throughout various locations in the app, where everything works fine. The only difference is that code from those locations doesn't call the method from inside an actor but from a class. Therefore, I believe it must be some sort of issue with actors.

EDIT:

The SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: false) is an obj-c function according to the documentation. Is there an issue with obj-c functions and actors by any chance?

Have you tried removing the calls inside createSCKSnapshot and just using your own async work temporarily? This could just be a bug with SCShareableContent's mapping into Swift from Obj-C. If that's the case you can try manually mapping the API into Swift using a continuation and the getExcludingDesktopWindows (yuck) version that takes a completion handler.

1 Like

Hey Jon! Thanks for your quick answer.

If I call the createSCKSnapshot method without any ScreenCaptureKit related code in it, everything seems to work fine. Since I unfortunately do need ScreenCaptureKit, I added the original code back, but with a continuation.

This is were things stop to work again - did I maybe do something wrong? I also removed the @MainActor before the function since it should not be necessary (I mainly put it there for debugging purposes anyways). Regardless of that, below code still doesn't work if I call it from my actor:

    public static func getShareableContent() async throws -> (displays: [SCDisplay], windows: [SCWindow], applications: [SCRunningApplication]) {
        let shareableContent: SCShareableContent = try await withCheckedThrowingContinuation { continuation in
            SCShareableContent.getExcludingDesktopWindows(false, onScreenWindowsOnly: false) { shareableContent, error in
                if let err = error {
                    continuation.resume(throwing: err)
                } else {
                    continuation.resume(returning: shareableContent!)
                }
            }
        }

        var windows: [SCWindow] = []

        for window in shareableContent.windows where
        window.owningApplication != nil
        && !window.owningApplication!.applicationName.isEmpty {
            windows.append(window)
        }
        
        return (shareableContent.displays, windows, shareableContent.applications)
    }

You'll want to see if the completion handler is ever called and, if so, whether execution continues after shareableContent is available.

1 Like

Yes, it is called and the rest of the code executes normally, even the return statement. But that then seems to get lost somewhere.

This is such a weird bug, I have never seen anything like this and I have no clue how to solve (or even debug) this. I went through it with the thread sanitizer as well (because I don't know any better) but no results there...

Any other idea on what I could try? The getShareableContent() method is called in other locations of my code too and there, no issues occur... This error explicitly only occurs when called from within an actor.

@linus-hologram can you try adding a defer block in your createSCKSnapshot? Just to make sure LLDB isn't outputting confusing data. If you have a release build in particular, it can be off a bit.

I was able to trace down the issue using Instrument's Swift Concurrency monitoring. The behavior likely arises due to the fact that our actor contains blocking code that never frees up the actor. Thereby, the return of the createSCKSnapshot was stuck in the actor queue indefinitely, waiting to be picked up by the (blocked) actor.

We still have to fix this in our code, and I'll update this thread once it is fixed (or other issues arise) for completeness.