Skip to content

Commit 24ae0bb

Browse files
Merge pull request #6 from Alexander-Ignition/swift_concurrency
Swift Concurrency
2 parents fa185bc + a3dbbb0 commit 24ae0bb

26 files changed

+1051
-485
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import PackageDescription
66
let package = Package(
77
name: "XSTooling",
88
platforms: [
9-
.macOS(.v10_13),
9+
.macOS(.v10_15),
1010
],
1111
products: [
1212
.library(

Playgrounds/README.playground/Contents.swift

Lines changed: 0 additions & 52 deletions
This file was deleted.

Playgrounds/README.playground/contents.xcplayground

Lines changed: 0 additions & 4 deletions
This file was deleted.

README.md

Lines changed: 92 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Supported tools:
1818
To use the `XSTooling` library in a SwiftPM project, add the following line to the dependencies in your `Package.swift` file:
1919

2020
```swift
21-
.package(url: "https://github.com/Alexander-Ignition/XSTooling", from: "0.0.1"),
21+
.package(url: "https://github.com/Alexander-Ignition/XSTooling", from: "0.0.2"),
2222
```
2323

2424
Include `"XSTooling"` as a dependency for your executable target:
@@ -34,25 +34,105 @@ Finally, add `import XSTooling` to your source code.
3434
```swift
3535
import XSTooling
3636

37-
let shell = Shell.sh
37+
let sh = Shell.default
38+
try await sh("swift build").run()
39+
```
40+
41+
## Shell
42+
43+
Shell command can be `run` or `read`.
44+
45+
Read the shell command output.
46+
47+
```swift
48+
let version = try await sh("xcodebuild -version").read().string
49+
```
50+
51+
Run shell command with redirection to stdout and stderr.
52+
53+
```swift
54+
try await sh("ls -al").run()
55+
```
56+
57+
Redirection can be configured, for example, to write to a log file.
58+
59+
```swift
60+
let url = URL(fileURLWithPath: "logs.txt", isDirectory: false)
61+
FileManager.default.createFile(atPath: url.path, contents: nil)
62+
let file = try FileHandle(forWritingTo: url)
63+
defer {
64+
try! file.close()
65+
}
66+
try await sh("swift build").run(.output(file).error(file))
67+
```
68+
69+
`Shell` has predefined instances.
70+
71+
```swift
72+
Shell.default
73+
Shell.sh
74+
Shell.bash
75+
Shell.zsh
76+
```
77+
78+
Conceptually, a `Shell` is a wrapper over a `ProcessCommand`.
3879

39-
let path = try shell("pwd").string
40-
let files = try shell("ls").lines
80+
- `sh.command` contains common parameters for all commands.
81+
- `sh("ls")` each call to this method returned a copy of the `ProcessCommand` with additional arguments
82+
83+
```swift
84+
sh = Shell.default
85+
sh.command // ProcessCommand
86+
sh.command.environment // [String: String]?
87+
sh.command.currentDirectoryURL // URL?
88+
sh("ls") // ProcessCommand
89+
```
90+
91+
## ProcessCommand
92+
93+
The main component is `ProcessCommand`. Which can configure and run a subprocess. The `read` and `run` methods are called on the `ProcessCommand`.
94+
95+
```swift
96+
let command = ProcessCommand(
97+
path: "/usr/bin/xcodebuild",
98+
arguments: ["-version"]
99+
)
100+
try await command.run()
101+
```
102+
103+
The location of the executable file is not always known. To do this, there is a `find` method that searches for an executable file by name.
104+
105+
```swift
106+
try await ProcessCommand
107+
.find("xcodebuild")!
108+
.appending(argument: "-version")
109+
.run()
41110
```
42111

43112
## simctl
44113

45-
Simulator control tool.
114+
By analogy with Shell, you can make other wrappers over the `ProcessCommand`
115+
116+
`Simctl` (Simulator control tool) is an example of a complex such wrapper
46117

47-
Fetch Extract the list of devices and filter them by the prefix of the name "iPhone".
118+
Using simctl, you can search for iPhone 12, turn it on and launch the application.
48119

49120
```swift
50121
let xcrun = XCRun()
51-
let simulator = try xcrun.simctl()
122+
let simulator = xcrun.simctl
123+
124+
let list = try await simulator.list(.devices, "iPhone 12", available: true).json.decode()
125+
let devices = list.devices.flatMap { $0.value }
126+
127+
for info in devices where info.state == "Booted" {
128+
try await simulator.device(info.udid).shutdown.run()
129+
}
130+
131+
let udid = devices.first!.udid
132+
try await simulator.device(udid).boot.run()
133+
try await simulator.device(udid).app("com.example.app").launch.run()
134+
```
52135

53-
let devices = try simulator.list().devices(where: { device in
54-
device.name.hasPrefix("iPhone")
55-
})
136+
## License
56137

57-
devices
58-
```
138+
MIT
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import Foundation
2+
3+
extension Process {
4+
var async: AsyncProcess {
5+
AsyncProcess(process: self)
6+
}
7+
}
8+
9+
final class AsyncProcess {
10+
private let _process: Process
11+
private let _lock = NSLock()
12+
13+
fileprivate init(process: Process) {
14+
self._process = process
15+
}
16+
17+
/// Runs the process with the current environment.
18+
func run() async throws {
19+
try await withTaskCancellationHandler {
20+
_terminate()
21+
} operation: {
22+
try Task.checkCancellation() // can be canceled before running
23+
try await _run()
24+
}
25+
}
26+
27+
private func _run() async throws {
28+
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
29+
var runningError: Error?
30+
_lock.lock()
31+
_process.terminationHandler = { _ in
32+
continuation.resume()
33+
}
34+
do {
35+
try _process.run() // waiting for the `terminationHandler` call
36+
} catch {
37+
// `terminationHandler` is not called if if an error occurred
38+
runningError = error
39+
}
40+
_lock.unlock()
41+
42+
if let runningError = runningError {
43+
continuation.resume(throwing: runningError)
44+
}
45+
}
46+
}
47+
48+
private func _terminate() {
49+
_lock.lock()
50+
if _process.isRunning { // can be canceled without starting
51+
_process.terminate() // crash if not running
52+
}
53+
_lock.unlock()
54+
}
55+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import Foundation
2+
3+
final class FileHandleReader {
4+
fileprivate(set) var data = Data()
5+
6+
fileprivate init() {}
7+
}
8+
9+
extension FileHandle {
10+
func reader() -> FileHandleReader {
11+
assert(self.readabilityHandler == nil)
12+
13+
let reader = FileHandleReader()
14+
15+
self.readabilityHandler = { fileHandle in
16+
// invoke on serial queue
17+
let data = fileHandle.availableData
18+
19+
if data.isEmpty {
20+
// stop
21+
fileHandle.readabilityHandler = nil
22+
} else {
23+
reader.data.append(data)
24+
}
25+
}
26+
return reader
27+
}
28+
}

Sources/XSTooling/Core/Kernel.swift

Lines changed: 0 additions & 63 deletions
This file was deleted.

0 commit comments

Comments
 (0)