session-ios/SignalUtilitiesKit/Utilities/OutageDetection.swift

128 lines
4.0 KiB
Swift

//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
import os
@objc
public class OutageDetection: NSObject {
@objc(sharedManager)
public static let shared = OutageDetection()
@objc public static let outageStateDidChange = Notification.Name("OutageStateDidChange")
// These properties should only be accessed on the main thread.
@objc
public var hasOutage = false {
didSet {
AssertIsOnMainThread()
if hasOutage != oldValue {
Logger.info("hasOutage: \(hasOutage).")
NotificationCenter.default.postNotificationNameAsync(OutageDetection.outageStateDidChange, object: nil)
}
}
}
private var shouldCheckForOutage = false {
didSet {
AssertIsOnMainThread()
ensureCheckTimer()
}
}
// We only show the outage warning when we're certain there's an outage.
// DNS lookup failures, etc. are not considered an outage.
private func checkForOutageSync() -> Bool {
let host = CFHostCreateWithName(nil, "uptime.signal.org" as CFString).takeRetainedValue()
CFHostStartInfoResolution(host, .addresses, nil)
var success: DarwinBoolean = false
guard let addresses = CFHostGetAddressing(host, &success)?.takeUnretainedValue() as NSArray? else {
Logger.error("CFHostGetAddressing failed: no addresses.")
return false
}
guard success.boolValue else {
Logger.error("CFHostGetAddressing failed.")
return false
}
var isOutageDetected = false
for case let address as NSData in addresses {
var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
if getnameinfo(address.bytes.assumingMemoryBound(to: sockaddr.self), socklen_t(address.length),
&hostname, socklen_t(hostname.count), nil, 0, NI_NUMERICHOST) == 0 {
let addressString = String(cString: hostname)
let kHealthyAddress = "127.0.0.1"
let kOutageAddress = "127.0.0.2"
if addressString == kHealthyAddress {
// Do nothing.
} else if addressString == kOutageAddress {
isOutageDetected = true
} else {
owsFailDebug("unexpected address: \(addressString)")
}
}
}
return isOutageDetected
}
private func checkForOutageAsync() {
Logger.info("")
DispatchQueue.global().async {
let isOutageDetected = self.checkForOutageSync()
DispatchQueue.main.async {
self.hasOutage = isOutageDetected
}
}
}
private var checkTimer: Timer?
private func ensureCheckTimer() {
// Only monitor for outages in the main app.
guard CurrentAppContext().isMainApp else {
return
}
if shouldCheckForOutage {
if checkTimer != nil {
// Already has timer.
return
}
// The TTL of the DNS record is 60 seconds.
checkTimer = WeakTimer.scheduledTimer(timeInterval: 60, target: self, userInfo: nil, repeats: true) { [weak self] _ in
AssertIsOnMainThread()
guard CurrentAppContext().isMainAppAndActive else {
return
}
guard let strongSelf = self else {
return
}
strongSelf.checkForOutageAsync()
}
} else {
checkTimer?.invalidate()
checkTimer = nil
}
}
@objc
public func reportConnectionSuccess() {
DispatchMainThreadSafe {
self.shouldCheckForOutage = false
self.hasOutage = false
}
}
@objc
public func reportConnectionFailure() {
DispatchMainThreadSafe {
self.shouldCheckForOutage = true
}
}
}