Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support off main thread #56

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
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
9 changes: 5 additions & 4 deletions .scripts/update-gh-pages.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ REPO=`git config remote.origin.url`

RELEASE="$TRAVIS_BRANCH"
PERMALINK="$SHA"
if [ "$TRAVIS_TAG" != "" ]; then
RELEASE="$TRAVIS_TAG"
PERMALINK="$TRAVIS_TAG"
TAG=$(git name-rev --tags --name-only $SHA)
if [ "$TAG" != "undefined" ]; then
RELEASE="$TAG"
PERMALINK="$TAG"
fi

if [ "$RELEASE" == "" ]; then exit 1; fi
Expand Down Expand Up @@ -53,7 +54,7 @@ pushd "$CHECKOUT_PATH"

# If this looks like a release tag, update the `latest` symlink to point to it.
LATEST_CANDIDATE_PATTERN='^[0-9]+[.][0-9]+[.][0-9]+$'
if [[ "$TRAVIS_TAG" =~ $LATEST_CANDIDATE_PATTERN ]]; then
if [[ "$TAG" =~ $LATEST_CANDIDATE_PATTERN ]]; then
echo " ==> Updating latest symlink to $RELEASE"
ln -sf "$RELEASE" "$DOC_URL_ROOT/latest"
git add "$DOC_URL_ROOT/latest"
Expand Down
2 changes: 1 addition & 1 deletion Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use_frameworks!
project 'Swindler'

target 'SwindlerTests' do
pod 'Quick', '~> 1.2.0'
pod 'Quick', git: 'https://github.com/pcantrell/Quick.git', branch: 'around-each'
pod 'Nimble', '~> 7.3.1'
end

Expand Down
21 changes: 14 additions & 7 deletions Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,37 @@ PODS:
- AXSwift (0.2.3)
- Nimble (7.3.1)
- PromiseKit/CorePromise (6.7.1)
- Quick (1.2.0)
- Quick (1.3.1)

DEPENDENCIES:
- AXSwift (from `./AXSwift`)
- Nimble (~> 7.3.1)
- PromiseKit/CorePromise (~> 6.0)
- Quick (~> 1.2.0)
- Quick (from `https://github.com/pcantrell/Quick.git`, branch `around-each`)

SPEC REPOS:
https://github.com/cocoapods/specs.git:
- Nimble
- PromiseKit
- Quick

EXTERNAL SOURCES:
AXSwift:
:path: "./AXSwift"
Quick:
:branch: around-each
:git: https://github.com/pcantrell/Quick.git

CHECKOUT OPTIONS:
Quick:
:commit: eeaddb112fc486b1e3699a5985e6a68dd5bc56d8
:git: https://github.com/pcantrell/Quick.git

SPEC CHECKSUMS:
AXSwift: d49fe05ca04f983196c5caedfc88f617922ae671
AXSwift: b84e5e2de171f1bf42ad5458ea9c6f0a73342c56
Nimble: 04f732da099ea4d153122aec8c2a88fd0c7219ae
PromiseKit: ef376fb8b4e92edfeb66bd403b983eaa07fbde0c
Quick: 58d203b1c5e27fff7229c4c1ae445ad7069a7a08
Quick: 741ba045f19fd3c783014e64882f8a72525661c8

PODFILE CHECKSUM: 43bf31a341af28c11e1feb4fd05f8c9670b59b58
PODFILE CHECKSUM: d10ed734b5c31159872da800ca78ce1012c9e781

COCOAPODS: 1.6.0.beta.2
COCOAPODS: 1.5.3
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ The following code assigns all windows on the screen to a grid. Note the simplic
promise-based API. Requests are dispatched concurrently and in the background, not serially.

```swift
Swindler.initialize().then { state -> Void in
Swindler.initialize().done { state in
let screen = state.screens.first!

let allPlacedOnGrid = screen.knownWindows.enumerate().map { index, window in
Expand Down
100 changes: 67 additions & 33 deletions Sources/Application.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,28 @@ import PromiseKit

/// A running application.
public final class Application {
internal let delegate: ApplicationDelegate
private let delegate_: ApplicationDelegate

// An Application holds a strong reference to the State (and therefore the StateDelegate).
// It should not be held internally by delegates, or it would create a reference cycle.
internal var state_: State!

private let queue_: DispatchQueue

var delegate: ApplicationDelegate {
dispatchPrecondition(condition: .onQueue(queue_))
return delegate_
}

var delegateUnchecked: ApplicationDelegate {
return delegate_
}

internal init(delegate: ApplicationDelegate, stateDelegate: StateDelegate) {
self.delegate = delegate
delegate_ = delegate
state_ = State(delegate: stateDelegate)
queue_ = delegate_.queue
dispatchPrecondition(condition: .onQueue(queue_))
}

/// This initializer only fails if the StateDelegate has been destroyed.
Expand Down Expand Up @@ -78,6 +91,8 @@ protocol ApplicationDelegate: class {
var focusedWindow: Property<OfOptionalType<Window>>! { get }
var isHidden: WriteableProperty<OfType<Bool>>! { get }

var queue: DispatchQueue { get }

func equalTo(_ other: ApplicationDelegate) -> Bool
}

Expand All @@ -102,7 +117,7 @@ final class OSXApplicationDelegate<

// Used internally for deferring code until an OSXWindowDelegate has been initialized for a
// given UIElement.
fileprivate var newWindowHandler = NewWindowHandler<UIElement>()
fileprivate var newWindowHandler: NewWindowHandler<UIElement>

fileprivate var initialized: Promise<Void>!

Expand All @@ -120,6 +135,8 @@ final class OSXApplicationDelegate<
return windows.map({ $0 as WindowDelegate })
}

let queue: DispatchQueue

/// Initializes the object and returns it as a Promise that resolves once it's ready.
static func initialize(
axElement: ApplicationElement,
Expand All @@ -139,7 +156,11 @@ final class OSXApplicationDelegate<
self.axElement = axElement.toElement
self.stateDelegate = stateDelegate
self.notifier = notifier
processIdentifier = try axElement.pid()
self.processIdentifier = try axElement.pid()

self.queue = stateDelegate.queue

self.newWindowHandler = NewWindowHandler<UIElement>(queue: queue)

let notifications: [AXNotification] = [
.windowCreated,
Expand Down Expand Up @@ -168,14 +189,16 @@ final class OSXApplicationDelegate<
MainWindowPropertyDelegate(axElement,
windowFinder: self,
windowDelegate: WinDelegate.self,
initProperties),
initProperties,
queue: queue),
withEvent: ApplicationMainWindowChangedEvent.self,
receivingObject: Application.self,
notifier: self)
focusedWindow = Property(
WindowPropertyAdapter(AXPropertyDelegate(axElement, .focusedWindow, initProperties),
windowFinder: self,
windowDelegate: WinDelegate.self),
windowDelegate: WinDelegate.self,
queue: queue),
withEvent: ApplicationFocusedWindowChangedEvent.self,
receivingObject: Application.self,
notifier: self)
Expand Down Expand Up @@ -210,7 +233,10 @@ final class OSXApplicationDelegate<
do {
weak var weakSelf = self
observer = try Observer(processID: processIdentifier, callback: { o, e, n in
weakSelf?.handleEvent(observer: o, element: e, notification: n)
guard let this = weakSelf else { return }
this.queue.async {
this.handleEvent(observer: o, element: e, notification: n)
}
})
} catch {
return Promise(error: error)
Expand Down Expand Up @@ -303,7 +329,7 @@ extension OSXApplicationDelegate {
fileprivate func handleEvent(observer: Observer.Context,
element: UIElement,
notification: AXSwift.AXNotification) {
assert(Thread.current.isMainThread)
dispatchPrecondition(condition: .onQueue(queue))
log.trace("Received \(notification) on \(element)")

switch notification {
Expand Down Expand Up @@ -484,21 +510,26 @@ extension OSXApplicationDelegate: CustomStringConvertible {

/// Stores internal new window handlers for OSXApplicationDelegate.
private struct NewWindowHandler<UIElement: Equatable> {
private let queue: DispatchQueue
fileprivate var handlers: [HandlerType<UIElement>] = []

init(queue: DispatchQueue) {
self.queue = queue
}

mutating func performAfterWindowCreatedForElement(_ windowElement: UIElement,
handler: @escaping () -> Void) {
assert(Thread.current.isMainThread)
dispatchPrecondition(condition: .onQueue(queue))
handlers.append(HandlerType(windowElement: windowElement, handler: handler))
}

mutating func removeAllForUIElement(_ windowElement: UIElement) {
assert(Thread.current.isMainThread)
dispatchPrecondition(condition: .onQueue(queue))
handlers = handlers.filter({ $0.windowElement != windowElement })
}

mutating func windowCreated(_ windowElement: UIElement) {
assert(Thread.current.isMainThread)
dispatchPrecondition(condition: .onQueue(queue))
handlers.filter({ $0.windowElement == windowElement }).forEach { entry in
entry.handler()
}
Expand Down Expand Up @@ -552,11 +583,13 @@ private final class MainWindowPropertyDelegate<
init(_ appElement: AppElement,
windowFinder: WinFinder,
windowDelegate: WinDelegate.Type,
_ initPromise: Promise<[Attribute: Any]>) {
_ initPromise: Promise<[Attribute: Any]>,
queue: DispatchQueue) {
readDelegate = WindowPropertyAdapter(
AXPropertyDelegate(appElement, .mainWindow, initPromise),
windowFinder: windowFinder,
windowDelegate: windowDelegate)
windowDelegate: windowDelegate,
queue: queue)
}

func initialize() -> Promise<Window?> {
Expand All @@ -569,18 +602,19 @@ private final class MainWindowPropertyDelegate<

func writeValue(_ newValue: Window) throws {
// Extract the element from the window delegate.
guard let winDelegate = newValue.delegate as? WinDelegate else {
// Note: This is happening on a background thread, so only properties that don't change
// should be accessed (the axElement).
guard let winDelegate = newValue.delegateUnchecked as? WinDelegate else {
throw PropertyError.illegalValue
}

// Check early to see if the element is still valid. If it becomes invalid after this check,
// the same error will get thrown, it will just take longer.
// Since this is just an optimization, it's okay if we read a stale `isValid` value.
guard winDelegate.isValid else {
throw PropertyError.illegalValue
}

// Note: This is happening on a background thread, so only properties that don't change
// should be accessed (the axElement).

// To set the main window, we have to access the .main attribute of the window element and
// set it to true.
let writeDelegate = AXPropertyDelegate<Bool, UIElement>(
Expand All @@ -602,16 +636,28 @@ private final class WindowPropertyAdapter<
let delegate: Delegate
weak var windowFinder: WinFinder?

init(_ delegate: Delegate, windowFinder: WinFinder, windowDelegate: WinDelegate.Type) {
let queue: DispatchQueue

init(_ delegate: Delegate,
windowFinder: WinFinder,
windowDelegate: WinDelegate.Type,
queue: DispatchQueue) {
self.delegate = delegate
self.windowFinder = windowFinder
self.queue = queue
}

func readValue() throws -> Window? {
guard let element = try delegate.readValue() else {
return nil
}
let window = findWindowByElement(element)

var window: Window?
dispatchPrecondition(condition: .notOnQueue(queue))
queue.sync {
window = self.windowFinder?.findWindowByElement(element)
}

if window == nil {
// This can happen if, for instance, the window was destroyed since the refresh was
// requested.
Expand All @@ -632,20 +678,8 @@ private final class WindowPropertyAdapter<
guard let element = maybeElement else {
return nil
}
return self.findWindowByElement(element)
}
}

fileprivate func findWindowByElement(_ element: Delegate.T) -> Window? {
// Avoid using locks by forcing calls out to `windowFinder` to happen on the main thead.
var window: Window?
if Thread.current.isMainThread {
window = windowFinder?.findWindowByElement(element)
} else {
DispatchQueue.main.sync {
window = self.windowFinder?.findWindowByElement(element)
}
dispatchPrecondition(condition: .onQueue(self.queue))
return self.windowFinder?.findWindowByElement(element)
}
return window
}
}
17 changes: 4 additions & 13 deletions Sources/FakeAXSwift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// TODO: Rename TestXyz classes to FakeXyz.

import AXSwift
import PromiseKit

/// A dictionary of AX attributes.
///
Expand Down Expand Up @@ -341,8 +342,9 @@ class FakeObserver: ObserverType {
passedElement: TestUIElement) {
let watched = watchedElements[watchedElement] ?? []
if watched.contains(notification) {
performOnMainThread {
callback(self, passedElement, notification)
// AX observer notifications always go to the run loop on the main thread.
DispatchQueue.main.async {
self.callback(self, passedElement, notification)
}
}
}
Expand Down Expand Up @@ -392,14 +394,3 @@ final private class WeakBox<A: AnyObject> {
unbox = value
}
}

/// Performs the given action on the main thread, synchronously, regardless of the current thread.
private func performOnMainThread(_ action: () -> Void) {
if Thread.current.isMainThread {
action()
} else {
DispatchQueue.main.sync {
action()
}
}
}
15 changes: 11 additions & 4 deletions Sources/FakeSwindler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ public class FakeState {
let appObserver = FakeApplicationObserver()
let screens = FakeSystemScreenDelegate(screens: screens.map{ $0.delegate })
return firstly {
Delegate.initialize(appObserver: appObserver, screens: screens)
Delegate.initialize(appObserver: appObserver, screens: screens,
queue: globalSwindlerQueue)
}.map { delegate in
FakeState(delegate, appObserver)
FakeState(delegate, appObserver, globalSwindlerQueue)
}
}

Expand All @@ -46,10 +47,15 @@ public class FakeState {
fileprivate var delegate: Delegate
var appObserver: FakeApplicationObserver

private init(_ delegate: Delegate, _ appObserver: FakeApplicationObserver) {
let queue: DispatchQueue

private init(_ delegate: Delegate,
_ appObserver: FakeApplicationObserver,
_ queue: DispatchQueue) {
self.state = State(delegate: delegate)
self.delegate = delegate
self.appObserver = appObserver
self.queue = queue
}
}

Expand Down Expand Up @@ -247,7 +253,8 @@ public class FakeScreen {
}

public init(frame: CGRect, applicationFrame: CGRect) {
delegate = FakeScreenDelegate(frame: frame, applicationFrame: applicationFrame)
delegate = FakeScreenDelegate(frame: frame, applicationFrame: applicationFrame,
queue: globalSwindlerQueue)
}
public convenience init(frame: CGRect, menuBarHeight: Int, dockHeight: Int) {
let af = CGRect(x: frame.origin.x,
Expand Down
Loading