Skip to content
This repository was archived by the owner on Oct 16, 2025. It is now read-only.
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
8 changes: 8 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,11 @@ jobs:
uses: norio-nomura/action-swiftlint@3.1.0
with:
args: --strict --path Manager
MacLessManagerSwiftLint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: GitHub Action for SwiftLint with --strict for MacLessManagerSwiftLint
uses: norio-nomura/action-swiftlint@3.1.0
with:
args: --strict --path MacLessManager
31 changes: 31 additions & 0 deletions .github/workflows/swift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,34 @@ jobs:
- name: Build
working-directory: Manager
run: swift build -v -c release
MacLessManagerBuildDebug:
runs-on: macos-latest
steps:
- uses: actions/checkout@v1
- name: Resolve
working-directory: MacLessManager
run: swift package resolve
- uses: actions/cache@v1
with:
path: MacLessManager/.build
key: ${{ runner.os }}-debug-spm-${{ hashFiles('MacLessManager/Package.resolved') }}
- name: Build
working-directory: MacLessManager
run: swift build -v -c debug
# - name: Test
# working-directory: MacLessManager
# run: swift test -v -c debug
MacLessManagerBuildRelease:
runs-on: macos-latest
steps:
- uses: actions/checkout@v1
- name: Resolve
working-directory: MacLessManager
run: swift package resolve
- uses: actions/cache@v1
with:
path: MacLessManager/.build
key: ${{ runner.os }}-release-spm-${{ hashFiles('MacLessManager/Package.resolved') }}
- name: Build
working-directory: MacLessManager
run: swift build -v -c release
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ RealDeviceMap-UIControl.xcodeproj/project.xcworkspace/xcuserdata/*
Manager/RDM-UIC-Manager.xcodeproj/
Manager/.build/
Manager/Package.resolved
Manager/.swiftpm/
MacLessManager/MacLessManager.xcodeproj/
MacLessManager/.build/
MacLessManager/Package.resolved
MacLessManager/.swiftpm/

# Pods
Podfile.lock
Expand Down
1 change: 1 addition & 0 deletions MacLessManager/.swift-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
5.0
8 changes: 8 additions & 0 deletions MacLessManager/.swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
included:
- Sources
disabled_rules:
- nesting
identifier_name:
excluded:
- i
- id
20 changes: 20 additions & 0 deletions MacLessManager/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// swift-tools-version:5.0

import PackageDescription

let package = Package(
name: "MacLessManager",
dependencies: [
.package(url: "https://github.com/apple/swift-log", from: "1.2.0"),
.package(url: "https://github.com/JohnSundell/ShellOut", from: "2.3.0")
],
targets: [
.target(
name: "MacLessManager",
dependencies: [
.product(name: "Logging", package: "swift-log"),
.product(name: "ShellOut", package: "ShellOut")
]
)
]
)
155 changes: 155 additions & 0 deletions MacLessManager/Sources/MacLessManager/MacLessManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import Foundation
import Logging
import ShellOut

class MacLessManager {

let logger: Logger
let id: String
let frontendURL: URL
let username: String
let password: String
let restartAfter: Double
let restartLockout: Double

let queue: DispatchQueue

let runningLock = NSLock()
var running: Bool = false
var deviceRestarts = [String: Date]()

init(frontendURL: String, username: String, password: String, restartAfter: Int, restartLockout: Int) {
self.id = UUID().uuidString
self.logger = Logger(label: "MacLessManager-\(id)")
self.frontendURL = URL(string: "\(frontendURL)/api/get_data?show_devices=true")!
self.password = password
self.username = username
self.restartAfter = Double(restartAfter)
self.restartLockout = Double(restartLockout)
self.queue = DispatchQueue(label: "MacLessManager-\(id)")
}

func start() {
runningLock.lock()
if !running {
running = true
runningLock.unlock()
logger.notice("Starting Manager")
queue.async {
self.run()
}
} else {
runningLock.unlock()
logger.info("Already Started")
}
}

func stop() {
runningLock.lock()
if running {
logger.notice("Stopping Manager")
running = false
} else {
logger.info("Already Stopped")
}
runningLock.unlock()
}

private func run() {
runningLock.lock()
while running {
runningLock.unlock()
guard let devices = try? getAllDevices(), !devices.isEmpty else {
self.logger.error("Failed to load devices (or none connected)")
sleep(1)
continue
}
self.logger.info("\(devices.count) devices connected")
guard let statusse = try? getAllDeviceStatusse(), !statusse.isEmpty else {
self.logger.error("Failed to load status")
sleep(1)
continue
}
self.logger.info("Loaded \(statusse.count) statusse")
for device in devices {
guard let status = statusse[device.value] else {
self.logger.error("No status for: \(device.value)")
continue
}
if Date().timeIntervalSince(status) >= restartAfter {
if let lastRestart = deviceRestarts[device.key],
Date().timeIntervalSince(lastRestart) <= restartLockout {
continue
}
do {
self.logger.notice(
"Restarting \(device.value). Last seen: \(Int(Date().timeIntervalSince(status)))s ago."
)
try restart(uuid: device.key)
deviceRestarts[device.key] = Date()
} catch {
self.logger.error("No status for: \(device.value)")
}
}
}
sleep(30)
runningLock.lock()
}
runningLock.unlock()
}

private func getAllDeviceStatusse() throws -> [String: Date] {
var request = URLRequest(url: frontendURL)
let token = "\(username):\(password)".data(using: .utf8)!.base64EncodedString()
request.addValue("Basic \(token)", forHTTPHeaderField: "Authorization")
let configuration = URLSessionConfiguration.default
configuration.httpCookieStorage = .shared
configuration.httpShouldSetCookies = true

var statusse = [String: Date]()
let semaphore = DispatchSemaphore(value: 0)
let task = URLSession(configuration: configuration).dataTask(with: request) { data, _, _ in
if let data = data,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let jsonData = json["data"] as? [String: Any],
let devices = jsonData["devices"] as? [[String: Any]] {
for device in devices {
if let uuid = device["uuid"] as? String, let seen = device["last_seen"] as? UInt32 {
statusse[uuid] = Date(timeIntervalSince1970: Double(seen))
}
}
}
semaphore.signal()
}
task.resume()
semaphore.wait()
return statusse
}

private func getAllDevices() throws -> [String: String] {
var devices = [String: String]()
let uuids = try getAllDeciceUUIDs()
for uuid in uuids {
let name = try? shellOut(to: "idevicename", arguments: ["--udid", uuid])
if name != nil {
devices[uuid] = name
}
}
return devices
}

private func getAllDeciceUUIDs() throws -> [String] {
let idString = try shellOut(to: "idevice_id", arguments: ["--list"])
guard !idString.isEmpty else {
return []
}
return idString.components(separatedBy: .newlines).map { (uuid) -> String in
return uuid.trimmingCharacters(in: .whitespaces)
}
}

private func restart(uuid: String) throws {
try shellOut(to: "idevicediagnostics", arguments: ["restart", "--udid", uuid])
}

}
64 changes: 64 additions & 0 deletions MacLessManager/Sources/MacLessManager/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import Foundation
import Logging

if CommandLine.arguments.contains("--help") ||
CommandLine.arguments.contains("-h") {
print("""
The following flags are available:
`--frontend url` (required) [The URL of the RDM frontend]
`--username username` (required) [The username of a RDM user with admin permission]
`--password password` (required) [The password of a RDM user with admin permission]
`--after time` (in seconds, default 120) [The time before an unseen device gets restarted]
`--lockout time` (in seconds, default 300) [The time to wait after restart untill another restart]
""")
exit(0)
}

guard let frontendURLIndex = CommandLine.arguments.firstIndex(of: "--frontend"),
CommandLine.arguments.count > frontendURLIndex + 1 else {
fatalError("--frontend not set but is required")
}
let frontendURL: String = CommandLine.arguments[Int(frontendURLIndex) + 1]

guard let usernameIndex = CommandLine.arguments.firstIndex(of: "--username"),
CommandLine.arguments.count > usernameIndex + 1 else {
fatalError("--username not set but is required")
}
let username: String = CommandLine.arguments[usernameIndex + 1]

guard let passwordIndex = CommandLine.arguments.firstIndex(of: "--password"),
CommandLine.arguments.count > passwordIndex + 1 else {
fatalError("--password not set but is required")
}
let password: String = CommandLine.arguments[passwordIndex + 1]

let restartAfter: Int
if let index = CommandLine.arguments.firstIndex(of: "--after"),
CommandLine.arguments.count > passwordIndex + 1,
let after = Int(CommandLine.arguments[index + 1]) {
restartAfter = after
} else {
restartAfter = 120
}

let restartLockout: Int
if let index = CommandLine.arguments.firstIndex(of: "--lockout"),
CommandLine.arguments.count > passwordIndex + 1,
let lockout = Int(CommandLine.arguments[index + 1]) {
restartLockout = lockout
} else {
restartLockout = 300
}

let manager = MacLessManager(
frontendURL: frontendURL,
username: username,
password: password,
restartAfter: restartAfter,
restartLockout: restartLockout
)
manager.start()

while true {
sleep(UInt32.max)
}
5 changes: 5 additions & 0 deletions MacLessManager/run.command
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
cd "`dirname "$0"`"
while true
do
swift run MacLessManager --backend https://map.url --username DeviceManager --password pass123
done