Skip to content

Commit

Permalink
fix: finished MVP of macOS swift-strategy, fixing lots of issues
Browse files Browse the repository at this point in the history
  • Loading branch information
ErikBjare committed Sep 26, 2022
1 parent fc423c1 commit 6d68ae3
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 37 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -5,3 +5,4 @@ build
dist
*.swp
.mypy_cache
aw-watcher-window-macos
9 changes: 9 additions & 0 deletions Makefile
Expand Up @@ -2,6 +2,15 @@

build:
poetry install
# if macOS, build swift
if [ "$(shell uname)" = "Darwin" ]; then \
make build-swift; \
fi

build-swift: aw-watcher-window-macos

aw-watcher-window-macos: aw_watcher_window/macos.swift
swiftc aw_watcher_window/macos.swift -o aw-watcher-window-macos

test:
aw-watcher-window --help
Expand Down
2 changes: 1 addition & 1 deletion aw_watcher_window/config.py
Expand Up @@ -42,7 +42,7 @@ def parse_args():
"--strategy",
dest="strategy",
default=default_strategy_macos,
choices=["jxa", "applescript"],
choices=["jxa", "applescript", "swift"],
help="(macOS only) strategy to use for retrieving the active window",
)
parsed_args = parser.parse_args()
Expand Down
91 changes: 62 additions & 29 deletions aw_watcher_window/macos.swift
Expand Up @@ -19,7 +19,7 @@ extension SBObject: ChromeWindow, ChromeTab {}

extension SBApplication: ChromeProtocol {}

struct NetworkMessage: Codable {
struct NetworkMessage: Codable, Equatable {
var app: String
var title: String
var url: String?
Expand All @@ -41,7 +41,7 @@ let clientName = "aw-watcher-window2"
let bucketName = "\(clientName)_\(clientHostname)"

let main = MainThing()
var oldHeartbeatData: NetworkMessage?
var oldHeartbeat: Heartbeat?

let encoder = JSONEncoder()
let formatter = ISO8601DateFormatter()
Expand Down Expand Up @@ -71,27 +71,36 @@ func start() {
name: NSWorkspace.didActivateApplicationNotification,
object: nil)
main.focusedAppChanged()
detectIdle()
//detectIdle()
}

// TODO: This will be unused for now, as aw-watcher-afk handles it
func detectIdle() {
let seconds = 15.0 - SystemIdleTime()!
if seconds < 0.0 {
sendHeartbeat(data: NetworkMessage(app: "", title: ""))
// TODO: read from config
let idletimeout = 3 * 60.0;
let untilidle = idletimeout - SystemIdleTime()!
if untilidle < 0.0 {
// Became idle

// TODO: send proper event
sendHeartbeat(Heartbeat(timestamp: Date.now, data: NetworkMessage(app: "", title: "")))

var monitor: Any?
monitor = NSEvent.addGlobalMonitorForEvents(matching: [
.mouseMoved, .leftMouseDown, .rightMouseDown, .keyDown,
]) { e in
print("User activity detected")
NSEvent.removeMonitor(monitor!)
if let oldMenu = oldHeartbeatData { sendHeartbeat(data: oldMenu) }
if let oldbeat = oldHeartbeat {
sendHeartbeat(Heartbeat(timestamp: Date.now, data: oldbeat.data))
}
detectIdle()
}

return
}

DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
DispatchQueue.main.asyncAfter(deadline: .now() + untilidle) {
detectIdle()
}
}
Expand All @@ -113,37 +122,62 @@ func createBucket() {
}
}

func sendHeartbeat(data: NetworkMessage) {
let payload0 = oldHeartbeatData.map {
try! encoder.encode(Heartbeat(timestamp: Date.now, data: $0))
func sendHeartbeat(_ heartbeat: Heartbeat) {
// First, send heartbeat ending last event, if event data differs
let payload_old: Heartbeat? = oldHeartbeat.flatMap {
if $0.data != heartbeat.data {
return Heartbeat(timestamp: heartbeat.timestamp, data: $0.data)
} else {
return Optional<Heartbeat>.none
}
}
let payload = try! encoder.encode(Heartbeat(timestamp: Date.now, data: data))

let url = URL(string: "http://localhost:5600/api/0/buckets/\(bucketName)/heartbeat?pulsetime=999999")!
// Then, send heartbeat starting new event
let payload_new = heartbeat

// TODO: set proper pulsetime
Task {
if let payload0 = payload0 {
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
let (_, response) = try await URLSession.shared.upload(for: urlRequest, from: payload0)
guard (200...299).contains((response as! HTTPURLResponse).statusCode) else {
print("Failed to send window exit heartbeat")
if let payload_old = payload_old {
let since_last_seconds = heartbeat.timestamp.timeIntervalSince(oldHeartbeat!.timestamp) + 1
do {
try await sendHeartbeatSingle(payload_old, pulsetime: since_last_seconds);
} catch {
print("Failed to send heartbeat: \(error)")
return
}
}

var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
let (_, response) = try await URLSession.shared.upload(for: urlRequest, from: payload)
guard (200...299).contains((response as! HTTPURLResponse).statusCode) else {
print("Failed to send window enter heartbeat")
do {
let since_last_seconds = heartbeat.timestamp.timeIntervalSince((oldHeartbeat ?? heartbeat).timestamp) + 1
try await sendHeartbeatSingle(payload_new, pulsetime: since_last_seconds);
} catch {
print("Failed to send heartbeat: \(error)")
return
}

// Assign the latest heartbeat as the old heartbeat
oldHeartbeat = heartbeat
}
}

enum HeartbeatError: Error {
case error(msg: String)
}

func sendHeartbeatSingle(_ heartbeat: Heartbeat, pulsetime: Double) async throws {
let url = URL(string: "http://localhost:5600/api/0/buckets/\(bucketName)/heartbeat?pulsetime=\(pulsetime)")!
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
let payload = try! encoder.encode(heartbeat)
let (_, response) = try await URLSession.shared.upload(for: urlRequest, from: payload)
guard (200...299).contains((response as! HTTPURLResponse).statusCode) else {
throw HeartbeatError.error(msg: "Failed to send heartbeat: \(response)")
}
// TODO: remove this debug logging when done
print("Sent heartbeat with timestamp: \(heartbeat.timestamp), pulsetime: \(round(pulsetime * 10) / 10), app: \(heartbeat.data.app), title: \(heartbeat.data.title)")
}

class MainThing {
var observer: AXObserver?
var oldWindow: AXUIElement?
Expand All @@ -154,7 +188,6 @@ class MainThing {
axElement: AXUIElement,
notification: CFString
) {

let frontmost = NSWorkspace.shared.frontmostApplication!
var windowTitle: AnyObject?
AXUIElementCopyAttributeValue(axElement, kAXTitleAttribute as CFString, &windowTitle)
Expand All @@ -175,8 +208,8 @@ class MainThing {
}
}

sendHeartbeat(data: data)
oldHeartbeatData = data
let heartbeat = Heartbeat(timestamp: Date.now, data: data)
sendHeartbeat(heartbeat)
}

@objc func focusedWindowChanged(_ observer: AXObserver, window: AXUIElement) {
Expand Down
21 changes: 14 additions & 7 deletions aw_watcher_window/main.py
Expand Up @@ -53,13 +53,20 @@ def main():

sleep(1) # wait for server to start
with client:
heartbeat_loop(
client,
bucket_id,
poll_time=args.poll_time,
strategy=args.strategy,
exclude_title=args.exclude_title,
)
if args.strategy == "swift":
logger.info("Using swift strategy, calling out to swift binary")
import subprocess

# TODO: pass config to swift code
subprocess.call(["./aw-watcher-window-macos"])
else:
heartbeat_loop(
client,
bucket_id,
poll_time=args.poll_time,
strategy=args.strategy,
exclude_title=args.exclude_title,
)


def heartbeat_loop(client, bucket_id, poll_time, strategy, exclude_title=False):
Expand Down

0 comments on commit 6d68ae3

Please sign in to comment.