Skip to content

Commit 8671d77

Browse files
Vladislav Alekseevbeefon
authored andcommitted
MTT-139: release 18
1 parent cc8d629 commit 8671d77

23 files changed

+952
-2
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
.DS_Store
2+
13
# Xcode
24
#
35
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
@@ -46,6 +48,7 @@ playground.xcworkspace
4648
# hence it is not needed unless you have added a package configuration file to your project
4749
# .swiftpm
4850

51+
.swiftpm/
4952
.build/
5053

5154
# CocoaPods

Package.resolved

Lines changed: 61 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// swift-tools-version:5.3
2+
import PackageDescription
3+
let package = Package(
4+
name: "EmceePluginSupport",
5+
platforms: [
6+
.macOS(.v11),
7+
],
8+
products: [
9+
.library(name: "EmceePlugin", targets: ["EmceePlugin"]),
10+
.library(name: "EmceePluginModels", targets: ["EmceePluginModels"]),
11+
],
12+
dependencies: [
13+
.package(name: "CommandLineToolkit", url: "https://github.com/avito-tech/CommandLineToolkit.git", from: "1.0.20"),
14+
.package(name: "Starscream", url: "https://github.com/daltoniam/Starscream.git", from: "3.0.6"),
15+
],
16+
targets: [
17+
.target(
18+
name: "EmceePlugin",
19+
dependencies: [
20+
.product(name: "CLTLogging", package: "CommandLineToolkit"),
21+
.product(name: "CLTLoggingModels", package: "CommandLineToolkit"),
22+
.product(name: "DateProvider", package: "CommandLineToolkit"),
23+
.product(name: "DI", package: "CommandLineToolkit"),
24+
.product(name: "FileSystem", package: "CommandLineToolkit"),
25+
.product(name: "JSONStream", package: "CommandLineToolkit"),
26+
.product(name: "Starscream", package: "Starscream"),
27+
.product(name: "SynchronousWaiter", package: "CommandLineToolkit"),
28+
"EmceePluginModels",
29+
]
30+
),
31+
.target(
32+
name: "EmceePluginModels"
33+
)
34+
]
35+
)

README.md

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,56 @@
1-
# EmceePluginSupport
2-
Swift package that allows to extend Emcee with plugins
1+
# Plugin
2+
3+
This module provides a basic class that plugin can use to create a bridge between the worker and the plugin.
4+
All events are broadcasted into running plugins, but not backwards.
5+
The worker will start web socket server, the plugin subprocess, and then disconnect.
6+
Plugin is expected to terminate after its web socket disconnect.
7+
Worker will forcefully terminate it after some timeout if it stays alive.
8+
9+
```
10+
Main Executable ----- (web socket) -------> Plugin
11+
```
12+
13+
## Using `Plugin`
14+
15+
In your `Package.swift`, import the library that has all required APIs to implement a plugin. Please use the matching Emcee version as a tag:
16+
17+
```
18+
dependencies: [
19+
.package(url: "https://github.com/avito-tech/EmceePluginSupport", .revision("18.0.0"))
20+
]
21+
```
22+
23+
In your plugin target add `EmceePlugin` dependency:
24+
25+
```
26+
targets: [
27+
.target(
28+
name: "MyOwnPlugin",
29+
dependencies: [
30+
"EmceePlugin",
31+
"EmceePluginModels"
32+
]
33+
)
34+
]
35+
```
36+
37+
The main class for plugin is `Plugin`. The most common scenario is (`main.swift`):
38+
39+
```swift
40+
import Foundation
41+
import EmceePlugin
42+
import EmceePluginModels
43+
44+
func main() throws -> Int32 {
45+
let plugin = try Plugin { (event: PluginAppleTestEvent) in
46+
// process an event
47+
}
48+
plugin.join()
49+
return 0
50+
}
51+
52+
exit(try main())
53+
54+
```
55+
56+
`Plugin` class will automatically stop streaming events when web socket gets disconnected, allowing `join()` method to return.

Sources/.DS_Store

6 KB
Binary file not shown.

Sources/EmceePlugin/.DS_Store

6 KB
Binary file not shown.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import CLTLogging
2+
import DateProvider
3+
import DI
4+
import FileSystem
5+
6+
public final class EmceeModuleDependencies: ModuleDependencies, InitializableWithNoArguments {
7+
public init() {}
8+
9+
public func otherModulesDependecies() -> [ModuleDependencies] {
10+
[
11+
DateProviderModuleDependencies(),
12+
FileSystemModuleDependencies(),
13+
]
14+
}
15+
16+
public func registerDependenciesOfCurrentModule(di: DependencyRegisterer) {
17+
di.register(type: LoggingSetup.self) { di in
18+
try LoggingSetup(
19+
dateProvider: di.resolve(),
20+
fileSystem: di.resolve(),
21+
logDomainName: "EmceePlugins"
22+
)
23+
}
24+
}
25+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright (c) Avito Tech LLC
3+
*/
4+
5+
import CLTLogging
6+
import Foundation
7+
import JSONStream
8+
import EmceePluginModels
9+
10+
final class EmceePluginJSONStreamAdapter: JSONReaderEventStream {
11+
private let logger: ContextualLogger
12+
private let decoder = JSONDecoder()
13+
private let onNewEvent: (PluginAppleTestEvent) -> ()
14+
15+
public init(
16+
logger: ContextualLogger,
17+
onNewEvent: @escaping (PluginAppleTestEvent) -> ()
18+
) {
19+
self.logger = logger
20+
self.onNewEvent = onNewEvent
21+
}
22+
23+
func newArray(_ array: NSArray, data: Data) {
24+
logger.warning("JSON stream reader received an unexpected event: '\(data)'")
25+
}
26+
27+
func newObject(_ object: NSDictionary, data: Data) {
28+
do {
29+
let event = try decoder.decode(PluginAppleTestEvent.self, from: data)
30+
onNewEvent(event)
31+
} catch {
32+
logger.error("Failed to decode plugin event: \(error), object: \(object)")
33+
}
34+
}
35+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright (c) Avito Tech LLC
3+
*/
4+
5+
import CLTLogging
6+
import EmceePluginModels
7+
import Foundation
8+
import Starscream
9+
10+
public final class EventReceiver: WebSocketDelegate {
11+
public typealias Handler = () -> ()
12+
public typealias ErrorHandler = (Swift.Error) -> ()
13+
public typealias DataHandler = (Data) -> ()
14+
15+
private let address: String
16+
private let logger: ContextualLogger
17+
private let pluginIdentifier: String
18+
private let socket: WebSocket
19+
private let encoder = JSONEncoder()
20+
private let decoder = JSONDecoder()
21+
private var didHandshake = false
22+
23+
public var onDisconnect: Handler?
24+
public var onError: ErrorHandler?
25+
public var onData: DataHandler?
26+
27+
public enum `Error`: Swift.Error, CustomStringConvertible {
28+
case acknowledgementError(message: String?)
29+
30+
public var description: String {
31+
switch self {
32+
case .acknowledgementError(let message):
33+
return "Plugin acknowledgement error: \(message ?? "null message")"
34+
}
35+
}
36+
}
37+
38+
public init(
39+
address: String,
40+
logger: ContextualLogger,
41+
pluginIdentifier: String
42+
) {
43+
self.address = address
44+
self.logger = logger
45+
self.pluginIdentifier = pluginIdentifier
46+
self.socket = WebSocket(url: URL(string: address)!)
47+
}
48+
49+
public func start() {
50+
logger.trace("Connecting to web socket: \(address)")
51+
didHandshake = false
52+
socket.delegate = self
53+
socket.connect()
54+
}
55+
56+
public func stop() {
57+
logger.trace("Disconnecting from web socket: \(address)")
58+
socket.disconnect()
59+
}
60+
61+
public func websocketDidConnect(socket: WebSocketClient) {
62+
logger.trace("Connected to web socket")
63+
do {
64+
let handshakeRequest = PluginHandshakeRequest(pluginIdentifier: pluginIdentifier)
65+
let data = try encoder.encode(handshakeRequest)
66+
socket.write(data: data)
67+
logger.trace("Sent handshake request")
68+
} catch {
69+
logger.error("Failed to encode handshake request: \(error)")
70+
}
71+
}
72+
73+
public func websocketDidDisconnect(socket: WebSocketClient, error: Swift.Error?) {
74+
didHandshake = false
75+
if let error = error {
76+
if let wsError = error as? WSError, wsError.code == CloseCode.normal.rawValue {
77+
logger.trace("Disconnected from web socket normally")
78+
onDisconnect?()
79+
} else {
80+
logger.error("Web socket error: \(error)")
81+
onError?(error)
82+
}
83+
} else {
84+
logger.trace("Disconnected from web socket without error")
85+
onDisconnect?()
86+
}
87+
}
88+
89+
public func websocketDidReceiveMessage(socket: WebSocketClient, text: String) {
90+
logger.warning("Received unexpected message from web socket: \(text)")
91+
}
92+
93+
public func websocketDidReceiveData(socket: WebSocketClient, data: Data) {
94+
logger.trace("Received data from web socket: \(data.count) bytes")
95+
96+
if didHandshake {
97+
onData?(data)
98+
} else if let acknowledgement = try? decoder.decode(PluginHandshakeAcknowledgement.self, from: data) {
99+
switch acknowledgement {
100+
case .successful:
101+
didHandshake = true
102+
case .error(let message):
103+
onError?(Error.acknowledgementError(message: message))
104+
}
105+
} else {
106+
stop()
107+
}
108+
}
109+
}

0 commit comments

Comments
 (0)