Emcee
178 строк · 6.9 Кб
1import EventBus
2import FileSystem
3import Foundation
4import EmceeLogging
5import PathLib
6import PluginSupport
7import ProcessController
8import ResourceLocationResolver
9import SynchronousWaiter
10
11public final class PluginManager: EventStream {
12private let encoder = JSONEncoder.pretty()
13private let eventDistributor: EventDistributor
14private let fileSystem: FileSystem
15private let logger: ContextualLogger
16private let pluginLocations: Set<AppleTestPluginLocation>
17private let pluginsConnectionTimeout: TimeInterval = 30.0
18private let processControllerProvider: ProcessControllerProvider
19private let resourceLocationResolver: ResourceLocationResolver
20private let sessionId = UUID()
21private let tearDownAllowance: TimeInterval = 60.0
22private var processControllers = [ProcessController]()
23
24public static let pluginBundleExtension = "emceeplugin"
25public static let pluginExecutableName = "Plugin"
26
27public init(
28fileSystem: FileSystem,
29logger: ContextualLogger,
30hostname: String,
31pluginLocations: Set<AppleTestPluginLocation>,
32processControllerProvider: ProcessControllerProvider,
33resourceLocationResolver: ResourceLocationResolver
34) {
35self.fileSystem = fileSystem
36self.logger = logger
37.withMetadata(key: "pluginSessionId", value: sessionId.uuidString)
38self.eventDistributor = EventDistributor(hostname: hostname, logger: logger)
39self.pluginLocations = pluginLocations
40self.processControllerProvider = processControllerProvider
41self.resourceLocationResolver = resourceLocationResolver
42}
43
44private func pathsToPluginBundles() throws -> [AbsolutePath] {
45var paths = [AbsolutePath]()
46for location in pluginLocations {
47let resolvableLocation = resourceLocationResolver.resolvable(withRepresentable: location)
48
49let validatePathToPluginBundle: (AbsolutePath) throws -> () = { path in
50guard path.lastComponent.pathExtension == PluginManager.pluginBundleExtension else {
51throw ValidationError.unexpectedExtension(location, actual: path.lastComponent.pathExtension, expected: PluginManager.pluginBundleExtension)
52}
53let executablePath = path.appending(PluginManager.pluginExecutableName)
54
55guard try self.fileSystem.properties(forFileAtPath: executablePath).isExecutable() else {
56throw ValidationError.noExecutableFound(location, expectedLocation: executablePath)
57}
58
59paths.append(path)
60}
61
62switch try resolvableLocation.resolve() {
63case .directlyAccessibleFile(let path):
64try validatePathToPluginBundle(path)
65case .contentsOfArchive(let containerPath, let concretePluginName):
66if let concretePluginName = concretePluginName {
67var path = containerPath.appending(concretePluginName)
68if path.lastComponent.pathExtension != PluginManager.pluginBundleExtension {
69path = path.appending(extension: PluginManager.pluginBundleExtension)
70}
71try validatePathToPluginBundle(path)
72} else {
73var availablePlugins = [AbsolutePath]()
74try fileSystem.contentEnumerator(forPath: containerPath, style: .shallow).each { path in
75if path.extension == PluginManager.pluginBundleExtension {
76availablePlugins.append(path)
77}
78}
79
80guard !availablePlugins.isEmpty else { throw ValidationError.noPluginsFound(location) }
81for path in availablePlugins {
82try validatePathToPluginBundle(path)
83}
84}
85}
86}
87return paths
88}
89
90public func startPlugins() throws {
91try eventDistributor.start()
92let pluginSocket = try eventDistributor.webSocketAddress()
93
94let pluginBundles = try pathsToPluginBundles()
95
96for bundlePath in pluginBundles {
97logger.trace("Starting plugin at \(bundlePath)")
98let pluginExecutable = bundlePath.appending(PluginManager.pluginExecutableName)
99let pluginIdentifier = try pluginExecutable.pathString.avito_sha256Hash()
100eventDistributor.add(pluginIdentifier: pluginIdentifier)
101let controller = try processControllerProvider.createProcessController(
102subprocess: Subprocess(
103arguments: [pluginExecutable],
104environment: environmentForLaunchingPlugin(
105pluginSocket: pluginSocket,
106pluginIdentifier: pluginIdentifier
107)
108)
109)
110try controller.start()
111processControllers.append(controller)
112}
113
114do {
115try eventDistributor.waitForPluginsToConnect(timeout: pluginsConnectionTimeout)
116} catch {
117logger.error("Failed to start plugins, will not tear down")
118tearDown()
119throw error
120}
121}
122
123private func environmentForLaunchingPlugin(pluginSocket: String, pluginIdentifier: String) -> Environment {
124[
125PluginSupport.pluginSocketEnv: pluginSocket,
126PluginSupport.pluginIdentifierEnv: pluginIdentifier
127]
128}
129
130private func killPlugins() {
131logger.trace("Killing plugins that are still alive")
132for controller in processControllers {
133controller.interruptAndForceKillIfNeeded()
134}
135processControllers.removeAll()
136}
137
138// MARK: - Event Stream
139
140public func process(event: BusEvent) {
141send(busEvent: event)
142switch event {
143case .appleRunnerEvent(let event):
144runnerEvent(event)
145case .tearDown:
146tearDown()
147}
148}
149
150private func runnerEvent(_ event: AppleRunnerEvent) {}
151
152private func tearDown() {
153do {
154try SynchronousWaiter().waitWhile(timeout: tearDownAllowance, description: "Tear down plugins") {
155processControllers.map { $0.isProcessRunning }.contains(true)
156}
157logger.trace("All plugins torn down successfully")
158} catch {
159killPlugins()
160}
161eventDistributor.stop()
162}
163
164// MARK: - Re-distributing events to the plugins
165
166private func send(busEvent: BusEvent) {
167do {
168let data = try encoder.encode(busEvent)
169sendData(data)
170} catch {
171logger.error("Failed to get data for \(busEvent) event: \(error)")
172}
173}
174
175private func sendData(_ data: Data) {
176eventDistributor.send(data: data)
177}
178}
179