Eine aufbereitete Darstellung der Quelle

 
     
 
 
Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 

Benutzer

Quelle  ProcessCoordinator.swift   Sprache: JAVA

 

import CocoaLumberjackSwift
import Foundation
import ThreemaEssentials

/// Coordinates the access to the shared resource (at this time only used for the connection to the server)
/// of message processing between the App, Notification Extension and Share Extension.
/// This should prevent concurrent message processing of several processes.
final class ProcessCoordinator: NSObject {

    private let notificationCenter = DarwinNotificationCenter.shared

    @objc public enum AccessState: Int, CaseIterable, CustomStringConvertible {
        /// Process doesn't access shared resource
        case unused = 0

        /// Process requested access to shared resource
        case requested

        /// Process accesses shared resource
        case using

        /// Process will release shared resource access
        case willRelease

        var description: String {
            switch self {
            case .unused: "unused"
            case .requested: "requested"
            case .using: "using"
            case .willRelease: "willRelease"
            }
        }
    }

    enum ProcessStateHandoverError: Error {
        case triesReached, noAnswer
    }

    // Dispatch queue to sync (serialize) request access calls
    private let startRequestQueue = DispatchQueue(
        label: "ch.threema.ProcessStateHandover.startRequestQueue",
        qos: .userInitiated
    )

    private var pollingSemaphore: DispatchSemaphore?
    private var pollingStopped = false
    private let pollingInMilliseconds = 70

    // Monotonic id of the currently running request cycle, guarded by `pollingQueue`.
    // Async closures (`processIPCMessage`, `pollingRequested`) captured by a previous cycle
    // compare against this to avoid acting on a newer cycle's polling primitives.
    private var currentRequestEpoch = 0

    // Dispatch queue to sync member variables `pollingSemaphore` and `pollingStopped`,
    // note that the polling is running in his own background thread
    private let pollingQueue = DispatchQueue(
        label: "ch.threema.ProcessStateHandover.pollingQueue",
        qos: .userInteractive,
        target: .global()
    )

    // Dispatch queue to sync access to the member variable `state` and `receivedMessageAfterRequest`
    private let updateStateQueue = DispatchQueue(label: "ch.threema.ProcessStateHandover.updateStateQueue")
    private var state: AccessState = .unused
    private var receivedMessageAfterRequest = false

    private let myAppType: AppGroupType
    private weak var serverConnector: ServerConnectorProtocol?
    private let userSettings: UserSettingsProtocol

    private var notificationsToObserve: [DarwinNotificationName] = []

    private var debugLastIncomingState: AccessState?
    private var debugPostNotificationName: DarwinNotificationName?

    required init(
        myAppType: AppGroupType,
        state: AccessState,
        serverConnector: ServerConnectorProtocol,
        userSettings: UserSettingsProtocol
    ) {
        self.myAppType = myAppType
        self.state = state
        self.serverConnector = serverConnector
        self.userSettings = userSettings

        super.init()

        initCommunication()
    }

    @objc convenience init(serverConnector: ServerConnectorProtocol, userSettings: UserSettingsProtocol) {
        self.init(
            myAppType: AppGroup.getCurrentType(),
            state: .unused,
            serverConnector: serverConnector,
            userSettings: userSettings
        )
    }

    deinit {
        for notification in notificationsToObserve {
            notificationCenter.removeObserver(name: notification)
        }

        serverConnector?.unregisterConnectionStateDelegate(delegate: self)
    }

    @available(swift, obsoleted: 1.0, renamed: "AccessState.description", message: "Only use from Objective-C")
    @objc static func nameFor(accessState state: AccessState) -> String {
        state.description
    }

    /// Request access to connecting to chat/mediator server.
    ///
    /// - Parameter pollingTimeout: If the timeout (milliseconds) reached
    ///  then the state will changed to `AccessState.using`.
    ///  If another process is running, the timeout should be higher than 700 ms.
    ///  This is because if the process is busy, the IPC message probably cannot be sent immediately.
    ///
    /// - Returns: Result as state
    @objc func requestAccess(pollingTimeout: Int) async -> AccessState {
        await withCheckedContinuation { continuation in
            startRequestQueue.async {
                guard self.serverConnector?.connectionState != .loggedIn else {
                    DDLogNotice("[Darwin] Already logged in")
                    continuation.resume(returning: .using)
                    return
                }

                continuation.resume(returning: self.requestAccess(timeout: pollingTimeout))
            }
        }
    }

    private func requestAccess(timeout: Int) -> AccessState {
        // Each request cycle owns its own semaphore. This is stored in `pollingSemaphore` so
        // `processIPCMessage` can signal it, but we wait on the local reference so a stale cycle
        // can never unblock a torn wait.
        let semaphore = DispatchSemaphore(value: 0)
        let epoch: Int = pollingQueue.sync {
            self.currentRequestEpoch += 1
            self.pollingSemaphore = semaphore
            self.pollingStopped = false
            return self.currentRequestEpoch
        }

        updateStateQueue.sync {
            self.receivedMessageAfterRequest = false
        }

        DispatchQueue.global(qos: .userInitiated).async {
            self.pollingRequested(epoch: epoch)
        }

        let result = semaphore.wait(timeout: .now() + .milliseconds(timeout))

        pollingQueue.sync {
            // Only tear down if we are still the active cycle, otherwise we would clobber a newer
            // request that started while stale closures were still running.
            if self.currentRequestEpoch == epoch {
                self.pollingStopped = true
                self.pollingSemaphore = nil
            }
        }

        if result == .timedOut {
            DDLogWarn("[Darwin] No answer to request access")

            updateStateQueue.sync {
                if !self.receivedMessageAfterRequest {
                    self.update(newState: .using)
                }
            }
        }

        // Read `state` on the same queue that writes it, so the returned value is never torn
        // against a concurrent `update(newState:)`.
        return updateStateQueue.sync {
            state
        }
    }

    // MARK: Private functions

    private func initCommunication() {
        serverConnector?.registerConnectionStateDelegate(delegate: self)

        /// Process received IPC message
        /// - Parameter state: IPC message type
        func processIPCMessage(_ state: AccessState, from appType: AppGroupType) {
            updateStateQueue.async {
                if self.debugLastIncomingState != state {
                    self.debugLastIncomingState = state
                    DDLogNotice(
                        "[Darwin] New incoming state: \(state) from: \(AppGroup.name(for: appType)) / actual state: \(self.state)"
                    )
                }
                self.receivedMessageAfterRequest = true

                let newState = self.process(state: state, from: appType)

                // Enforce posting on `request`, otherwise the sender will be connect after a defined timeout
                self.update(newState: newState, enforcePosting: state == .requested)

                self.pollingQueue.sync {
                    self.pollingStopped = true
                    _ = self.pollingSemaphore?.signal()
                }
            }
        }

        // Initializes observers for all other App types in Darwin Notification Center
        for appType in [AppGroupTypeApp, AppGroupTypeShareExtension, AppGroupTypeNotificationExtension] {
            if appType != myAppType {
                for state in AccessState.allCases {
                    let name: DarwinNotificationName = .notificationName(
                        appType: appType,
                        secret: secretPrefix,
                        state: state
                    )

                    notificationCenter.addObserver(name: name) { _ in
                        processIPCMessage(state, from: appType)
                    }

                    notificationsToObserve.append(name)
                }
            }
        }
    }

    private func pollingRequested(epoch: Int) {
        func pollingRequestedRecursive() {
            updateStateQueue.sync {
                guard self.state != .using else {
                    self.pollingQueue.sync {
                        if self.currentRequestEpoch == epoch {
                            self.pollingStopped = true
                        }
                    }
                    return
                }

                self.update(newState: .requested, enforcePosting: true)
            }
        }

        DispatchQueue.global(qos: .userInitiated)
            .asyncAfter(deadline: .now() + .milliseconds(pollingInMilliseconds)) {
                // Stop if polling was stopped or a newer request cycle superseded this one.
                let stopped = self.pollingQueue.sync {
                    self.pollingStopped || self.currentRequestEpoch != epoch
                }

                if !stopped {
                    pollingRequestedRecursive()

                    self.pollingRequested(epoch: epoch)
                }
            }
    }

    /// Update state with new state and post if it has changed or if the state is `using`.
    /// - Parameters:
    ///   - newState: The new state to update
    ///   - enforcePosting: Enforce posting of the actual state
    private func update(newState: AccessState, enforcePosting: Bool = false) {
        var doPost = false
        if state != newState {
            DDLogNotice(
                "[Darwin] Set state from \(state) to \(newState)"
            )
            state = newState
            doPost = true
        }
        if doPost || enforcePosting || state == .using {
            let name: DarwinNotificationName = .notificationName(
                appType: myAppType,
                secret: secretPrefix,
                state: state
            )

            if self.debugPostNotificationName != name {
                self.debugPostNotificationName = name
                DDLogNotice("[Darwin] Post \(name)")
            }

            notificationCenter.post(name)
        }
    }

    #if DEBUG
        /// Function for unit tests
        func testProcess(
            state receivedState: AccessState,
            from appType: AppGroupType = AppGroupTypeApp
        ) -> AccessState {
            process(state: receivedState, from: appType)
        }
    #endif

    private func process(state receivedState: AccessState, from fromAppType: AppGroupType) -> AccessState {

        guard fromAppType != myAppType else {
            DDLogError(
                "[Darwin] Received state '\(receivedState)' from self app '\(AppGroup.name(for: myAppType))'"
            )
            return state
        }

        let newState: AccessState

        switch receivedState {
        case .unused:
            switch state {
            case .unused: newState = .unused
            case .requested: newState = .using
            case .using: newState = .using
            case .willRelease: newState = .willRelease
            }
        case .requested:
            switch state {
            case .unused: newState = .unused
            case .requested:
                switch myAppType {
                case AppGroupTypeApp: newState = fromAppType == AppGroupTypeShareExtension ? .willRelease : .requested
                case AppGroupTypeShareExtension: newState = .requested
                case AppGroupTypeNotificationExtension: newState = .willRelease
                default:
                    fatalError("Unknown my app type: \(myAppType)")
                }
            case .using:
                switch myAppType {
                case AppGroupTypeApp:
                    if fromAppType == AppGroupTypeShareExtension {
                        newState = .willRelease

                        // In this case the app and the share extension run at the same time.
                        // This can happen when a file message is shared within the app. Then the app
                        // must disconnect and reconnect with a 2 seconds delay.
                        serverConnector?.disconnect(initiator: .app, force: true)
                        DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
                            self.serverConnector?.connect(initiator: .app)
                        }
                    }
                    else {
                        newState = .using
                    }
                case AppGroupTypeShareExtension: newState = .using
                case AppGroupTypeNotificationExtension: newState = .willRelease
                default:
                    fatalError("Unknown my app type: \(myAppType)")
                }
            case .willRelease: newState = .willRelease
            }
        case .using:
            switch state {
            case .unused: newState = .unused
            case .requested:
                switch myAppType {
                case AppGroupTypeApp: newState = fromAppType == AppGroupTypeShareExtension ? .willRelease : .requested
                case AppGroupTypeShareExtension: newState = .requested
                case AppGroupTypeNotificationExtension: newState = .willRelease
                default:
                    fatalError("Unknown my app type: \(myAppType)")
                }
            case .using:
                if myAppType == AppGroupTypeApp {
                    newState = .using
                }
                else {
                    newState = .willRelease

                    DDLogWarn(
                        "[Darwin] Disconnect me (notification- or share extension) immediately to resolve the `using` collision"
                    )
                    serverConnector?.disconnect(
                        initiator: connectionInitiator(for: myAppType),
                        force: true
                    )
                }
            case .willRelease:
                newState = .willRelease

                DDLogWarn("[Darwin] Disconnect me immediately to prevent duplicate connection")
                serverConnector?.disconnect(
                    initiator: connectionInitiator(for: myAppType),
                    force: true
                )
            }
        case .willRelease:
            switch state {
            case .unused: newState = .unused
            case .requested: newState = .requested
            case .using: newState = .using
            case .willRelease: newState = .willRelease
            }
        }

        return newState
    }

    private func connectionInitiator(for appGroupType: AppGroupType) -> ConnectionInitiator {
        switch appGroupType {
        case AppGroupTypeApp, AppGroupTypeNone: .app
        case AppGroupTypeShareExtension: .shareExtension
        case AppGroupTypeNotificationExtension: .notificationExtension
        default:
            fatalError("Unknown my app type: \(appGroupType)")
        }
    }

    private var secretPrefix: String {
        if let secret = userSettings.ipcSecretPrefix, !secret.isEmpty {
            return secret.hexString
        }

        guard let secret = BytesUtility.generateRandomBytes(length: 8) else {
            fatalError("Failed to generate random bytes for IPC secret")
        }

        userSettings.ipcSecretPrefix = secret

        return secret.hexString
    }
}

extension DarwinNotificationName {
    private static let appIdentifier = BundleUtil.threemaAppIdentifier() ?? "ch.threema"

    private static func appPrefix(_ appType: AppGroupType) -> String {
        "\(appIdentifier).\(AppGroup.name(for: appType))"
    }

    static func notificationName(
        appType: AppGroupType,
        secret: String,
        state: ProcessCoordinator.AccessState
    ) -> DarwinNotificationName {
        DarwinNotificationName("\(appPrefix(appType))-\(secret)-\(state)")
    }
}

// MARK: - ProcessCoordinator + ConnectionStateDelegate

extension ProcessCoordinator: ConnectionStateDelegate {
    func changed(connectionState state: ConnectionState) {
        updateStateQueue.async {
            switch state {
            case .connecting, .connected, .loggedIn:
                DDLogNotice("[Darwin] Connected")
                self.update(newState: .using)
            case .disconnecting:
                DDLogNotice("[Darwin] Disconnecting")
                self.update(newState: .willRelease)
            case .disconnected:
                DDLogNotice("[Darwin] Disconnected")
                self.update(newState: .unused)
            }
        }
    }
}

Messung
CHZG
869410093

¤ Dauer der Verarbeitung: 0.4 Sekunden  (vorverarbeitet am  2026-11-04) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover


Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Open Source Software

     Quellcodebibliothek
     Eigene Quellcodes
     Fremde Quellcodes
     Suchen

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Statistik
#Sources=1104852
#Domains=417151