diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 209d2dd6..3407be10 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -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 diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 34a050cc..dd0dacd2 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -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 diff --git a/.gitignore b/.gitignore index d2a92f5b..ac6d9348 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/MacLessManager/.swift-version b/MacLessManager/.swift-version new file mode 100644 index 00000000..819e07a2 --- /dev/null +++ b/MacLessManager/.swift-version @@ -0,0 +1 @@ +5.0 diff --git a/MacLessManager/.swiftlint.yml b/MacLessManager/.swiftlint.yml new file mode 100644 index 00000000..b07b509c --- /dev/null +++ b/MacLessManager/.swiftlint.yml @@ -0,0 +1,8 @@ +included: + - Sources +disabled_rules: +- nesting +identifier_name: + excluded: + - i + - id diff --git a/MacLessManager/Package.swift b/MacLessManager/Package.swift new file mode 100644 index 00000000..b658c9fa --- /dev/null +++ b/MacLessManager/Package.swift @@ -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") + ] + ) + ] +) diff --git a/MacLessManager/Sources/MacLessManager/MacLessManager.swift b/MacLessManager/Sources/MacLessManager/MacLessManager.swift new file mode 100644 index 00000000..008a14cd --- /dev/null +++ b/MacLessManager/Sources/MacLessManager/MacLessManager.swift @@ -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]) + } + +} diff --git a/MacLessManager/Sources/MacLessManager/main.swift b/MacLessManager/Sources/MacLessManager/main.swift new file mode 100644 index 00000000..a67fc141 --- /dev/null +++ b/MacLessManager/Sources/MacLessManager/main.swift @@ -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) +} diff --git a/MacLessManager/run.command b/MacLessManager/run.command new file mode 100644 index 00000000..ba537230 --- /dev/null +++ b/MacLessManager/run.command @@ -0,0 +1,5 @@ +cd "`dirname "$0"`" +while true +do + swift run MacLessManager --backend https://map.url --username DeviceManager --password pass123 +done