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 5 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/[email protected]
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/[email protected]
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", .upToNextMinor(from: "1.2.0")),
.package(url: "https://github.com/JohnSundell/ShellOut", .upToNextMinor(from: "2.3.0"))
],
targets: [
.target(
name: "MacLessManager",
dependencies: [
.product(name: "Logging", package: "swift-log"),
.product(name: "ShellOut", package: "ShellOut")
]
)
]
)
153 changes: 153 additions & 0 deletions MacLessManager/Sources/MacLessManager/MacLessManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
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 laod devices (or none connected)")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

load

sleep(1)
continue
}
self.logger.info("\(devices.count) devices connected")
guard let statusse = try? getAllDeviceStatusse(), !statusse.isEmpty else {
self.logger.error("Failed to laod statusse")
Copy link

@clburlison clburlison May 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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])
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 = CommandLine.arguments[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 = 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 = 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)
}