Skip to content

Commit 93b5456

Browse files
authored
Merge pull request #41 from Automattic/add/console-output-helper
Add Console Output Helper
2 parents d93260d + 12c9e83 commit 93b5456

File tree

10 files changed

+366
-54
lines changed

10 files changed

+366
-54
lines changed

Package.resolved

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

Package.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ let package = Package(
1313
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.1.4"),
1414
.package(url: "https://github.com/soto-project/soto.git", from: "6.0.0"),
1515
.package(url: "https://github.com/jkmassel/prlctl.git", from: "1.17.0"),
16-
.package(url: "https://github.com/ebraraktas/swift-tqdm.git", from: "0.1.2"),
1716
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
1817
.package(url: "https://github.com/jkmassel/kcpassword-swift.git", from: "1.0.0"),
1918
.package(url: "https://github.com/swiftpackages/DotEnv.git", from: "3.0.0"),
2019
.package(url: "https://github.com/apple/swift-tools-support-core", from: "0.2.5"),
21-
.package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.6.1"))
20+
.package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.6.1")),
21+
.package(url: "https://github.com/vapor/console-kit.git", .upToNextMajor(from: "4.5.0")),
2222
],
2323
targets: [
2424
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
@@ -28,7 +28,6 @@ let package = Package(
2828
dependencies: [
2929
.product(name: "ArgumentParser", package: "swift-argument-parser"),
3030
.product(name: "prlctl", package: "prlctl"),
31-
.product(name: "Tqdm", package: "swift-tqdm"),
3231
.product(name: "Logging", package: "swift-log"),
3332
.product(name: "kcpassword", package: "kcpassword-swift"),
3433
.target(name: "libhostmgr"),
@@ -41,6 +40,7 @@ let package = Package(
4140
.product(name: "SotoS3", package: "soto"),
4241
.product(name: "TSCBasic", package: "swift-tools-support-core"),
4342
.product(name: "Alamofire", package: "Alamofire"),
43+
.product(name: "ConsoleKit", package: "console-kit"),
4444
]
4545
),
4646
.testTarget(

Sources/hostmgr/commands/benchmark/NetworkBenchmark.swift

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,32 +27,16 @@ struct NetworkBenchmark: AsyncParsableCommand {
2727
region: Configuration.shared.vmImagesRegion
2828
)
2929

30+
let progressBar = Console.startFileDownload(file.imageObject)
31+
3032
try await manager.download(
3133
object: file.imageObject,
3234
to: FileManager.default.temporaryFilePath(),
33-
progressCallback: self.updateProgress
35+
progressCallback: progressBar.update
3436
)
3537
}
3638

3739
private func imageSizeSort(_ lhs: RemoteVMImage, _ rhs: RemoteVMImage) -> Bool {
3840
lhs.imageObject.size < rhs.imageObject.size
3941
}
40-
41-
private func updateProgress(_ progress: FileTransferProgress) {
42-
Self.limiter.perform {
43-
let downloadedSize = ByteCountFormatter.string(fromByteCount: Int64(progress.current), countStyle: .file)
44-
let totalSize = ByteCountFormatter.string(fromByteCount: Int64(progress.total), countStyle: .file)
45-
46-
let secondsElapsed = Date().timeIntervalSince(startDate)
47-
let perSecond = Double(progress.current) / Double(secondsElapsed)
48-
49-
// Don't continue unless the rate can be represented by `Int64`
50-
guard perSecond.isNormal else {
51-
return
52-
}
53-
54-
let rate = ByteCountFormatter.string(fromByteCount: Int64(perSecond), countStyle: .file)
55-
logger.info("Downloaded \(downloadedSize) of \(totalSize) [Rate: \(rate) per second]")
56-
}
57-
}
5842
}

Sources/hostmgr/commands/sync/SyncVMImagesCommand.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,12 @@ struct SyncVMImagesCommand: AsyncParsableCommand, FollowsCommandPolicies {
6060

6161
let limiter = Limiter(policy: .throttle, operationsPerSecond: 1)
6262

63+
let progressBar = Console.startImageDownload(image)
6364
try await RemoteVMRepository().download(image: image, to: destination) { progress in
65+
progressBar.update(progress)
66+
6467
limiter.perform {
6568
try? recordHeartbeat()
66-
logger.info("\(progress.decimalPercent)% complete")
6769
}
6870
}
6971

Sources/hostmgr/commands/vm/image/remote/VMRemoteImageDownload.swift

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import Foundation
22
import ArgumentParser
3-
import Tqdm
43
import libhostmgr
54

65
struct VMRemoteImageDownload: AsyncParsableCommand {
@@ -48,17 +47,10 @@ struct VMRemoteImageDownload: AsyncParsableCommand {
4847
let sleepManager = SystemSleepManager(reason: "Downloading \(remoteImage.fileName)")
4948
sleepManager.disable()
5049

51-
let progressBar = Tqdm(
52-
description: "Downloading \(remoteImage.fileName)",
53-
total: Int(remoteImage.imageObject.size),
54-
unit: " bytes",
55-
unitScale: true
56-
)
50+
let progressBar = Console.startImageDownload(remoteImage)
5751

5852
try await remote.download(image: remoteImage, to: destination) {
59-
progressBar.update(n: $0.percent)
53+
progressBar.update($0)
6054
}
61-
62-
progressBar.close()
6355
}
6456
}
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import Foundation
2+
import ConsoleKit
3+
4+
public struct Console {
5+
6+
let terminal = Terminal()
7+
8+
@discardableResult public func heading(_ message: String, underline: String = "=") -> Self {
9+
self.terminal.output(message, style: .plain)
10+
self.terminal.output(String.init(repeating: underline, count: message.count), style: .plain)
11+
return self
12+
}
13+
14+
@discardableResult public func success(_ message: String) -> Self {
15+
self.terminal.success(message)
16+
return self
17+
}
18+
19+
@discardableResult public func error(_ message: String) -> Self {
20+
self.terminal.error(message)
21+
return self
22+
}
23+
24+
@discardableResult public func warn(_ message: String) -> Self {
25+
self.terminal.warning(message)
26+
return self
27+
}
28+
29+
@discardableResult public func info(_ message: String) -> Self {
30+
self.terminal.info(message)
31+
return self
32+
}
33+
34+
@discardableResult public func log(_ message: String) -> Self {
35+
self.terminal.print(message)
36+
return self
37+
}
38+
39+
@discardableResult func message(_ message: String, style: ConsoleStyle) -> Self {
40+
self.terminal.output(message, style: style)
41+
return self
42+
}
43+
44+
@discardableResult public func printList(_ list: [String], title: String) -> Self {
45+
self.terminal.print(title)
46+
47+
guard !list.isEmpty else {
48+
self.terminal.print(" [Empty]")
49+
return self
50+
}
51+
52+
for item in list {
53+
self.terminal.print(" " + item)
54+
}
55+
56+
return self
57+
}
58+
59+
@discardableResult public func printTable(
60+
data: Table,
61+
columnTitles: [String] = [],
62+
columnSeparator: String = " "
63+
) -> Self {
64+
65+
if data.isEmpty {
66+
self.terminal.print("[Empty]")
67+
return self
68+
}
69+
70+
// Prepend the Column Titles, if present
71+
let table = columnTitles.isEmpty ? data : [columnTitles] + data
72+
73+
let columnCount = columnCounts(for: table)
74+
75+
for row in table {
76+
let string = zip(row, columnCount).map(self.padString).joined(separator: columnSeparator)
77+
self.terminal.print(string)
78+
}
79+
80+
return self
81+
}
82+
}
83+
84+
public struct ProgressBar {
85+
86+
private let terminal = Terminal()
87+
private let startDate = Date()
88+
89+
init(title: String) {
90+
terminal.info(title)
91+
terminal.print() // Deliberately empty string
92+
}
93+
94+
public static func start(title: String) -> Self {
95+
return ProgressBar(title: title)
96+
}
97+
98+
public func update(_ progress: FileTransferProgress) {
99+
terminal.clear(lines: 1)
100+
101+
let elapsedTime = Date().timeIntervalSince(startDate)
102+
103+
let rate = progress.dataRate(timeIntervalSinceStart: elapsedTime)
104+
let remaining = progress.estimatedTimeRemaining(timeIntervalSinceStart: elapsedTime)
105+
106+
terminal.print("\(progress.formattedPercentage)% [\(rate)/s, \(Format.time(remaining))]")
107+
}
108+
}
109+
110+
// MARK: Static Helpers
111+
extension Console {
112+
public static func startProgress(_ string: String) -> ProgressBar {
113+
ProgressBar(title: string)
114+
}
115+
116+
public static func startImageDownload(_ image: RemoteVMImage) -> ProgressBar {
117+
let size = Format.fileBytes(image.imageObject.size)
118+
return ProgressBar(title: "Downloading \(image.fileName) (\(size))")
119+
}
120+
121+
public static func startFileDownload(_ file: S3Object) -> ProgressBar {
122+
let size = Format.fileBytes(file.size)
123+
return ProgressBar(title: "Downloading \(file.key) (\(size))")
124+
}
125+
}
126+
127+
// MARK: Static Initializers
128+
extension Console {
129+
@discardableResult public static func heading(_ message: String) -> Self {
130+
return Console().heading(message)
131+
}
132+
133+
@discardableResult public static func success(_ message: String) -> Self {
134+
return Console().success(message)
135+
}
136+
137+
@discardableResult public static func error(_ message: String) -> Self {
138+
return Console().error(message)
139+
}
140+
141+
@discardableResult public static func warn(_ message: String) -> Self {
142+
return Console().warn(message)
143+
}
144+
145+
@discardableResult public static func info(_ message: String) -> Self {
146+
return Console().info(message)
147+
}
148+
149+
@discardableResult public static func log(_ message: String) -> Self {
150+
return Console().log(message)
151+
}
152+
153+
@discardableResult public static func printList(_ list: [String], title: String) -> Self {
154+
return Console().printList(list, title: title)
155+
}
156+
157+
@discardableResult public static func printTable(data: Table, columnTitles: [String] = []) -> Self {
158+
return Console().printTable(data: data, columnTitles: columnTitles)
159+
}
160+
161+
public static func crash(message: String, reason error: ExitCode) -> Never {
162+
Console().error(message)
163+
Foundation.exit(error.rawValue)
164+
}
165+
166+
public static func exit(message: String = "", style: ConsoleStyle = .plain) -> Never {
167+
Console().message(message, style: style)
168+
Foundation.exit(0)
169+
}
170+
}
171+
172+
// MARK: Table Support
173+
extension Console {
174+
175+
public typealias TableRow = [String]
176+
public typealias Table = [TableRow]
177+
178+
func columnCounts(for table: Table) -> [Int] {
179+
transpose(matrix: table).map { $0.map(\.count).max() ?? 0 }
180+
}
181+
182+
func transpose(matrix: Table) -> Table {
183+
guard let numberOfColumns = matrix.first?.count else {
184+
return matrix
185+
}
186+
187+
var newTable = Table(repeating: TableRow(repeating: "", count: matrix.count), count: numberOfColumns)
188+
189+
for (rowIndex, row) in matrix.enumerated() {
190+
for (colIndex, col) in row.enumerated() {
191+
newTable[colIndex][rowIndex] = col
192+
}
193+
}
194+
195+
return newTable
196+
}
197+
198+
func padString(_ string: String, toLength length: Int) -> String {
199+
string.padding(toLength: length, withPad: " ", startingAt: 0)
200+
}
201+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import Foundation
2+
3+
public enum ExitCode: Int32, Error {
4+
case fileNotFound
5+
case unableToFindRemoteImage
6+
case unableToImportVM
7+
case invalidVMStatus
8+
case notEnoughLocalDiskSpace
9+
case parallelsVirtualMachineDoesNotExist
10+
case parallelsVirtualMachineIsNotStopped
11+
case parallelsVirtualMachineAlreadyExists
12+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import Foundation
2+
3+
public struct Format {
4+
5+
public static func fileBytes(_ count: Int) -> String {
6+
fileBytes(Int64(count))
7+
}
8+
9+
public static func fileBytes(_ count: Int64) -> String {
10+
let formatter = ByteCountFormatter()
11+
formatter.zeroPadsFractionDigits = true
12+
formatter.countStyle = .file
13+
return formatter.string(fromByteCount: count)
14+
}
15+
16+
public static func memoryBytes(_ count: UInt64) -> String {
17+
memoryBytes(Int64(count))
18+
}
19+
20+
public static func memoryBytes(_ count: Int64) -> String {
21+
let formatter = ByteCountFormatter()
22+
formatter.zeroPadsFractionDigits = true
23+
formatter.countStyle = .memory
24+
return formatter.string(fromByteCount: count)
25+
}
26+
27+
public static func time(_ interval: TimeInterval) -> String {
28+
let formatter = RelativeDateTimeFormatter()
29+
formatter.formattingContext = .standalone
30+
return formatter.localizedString(fromTimeInterval: interval)
31+
}
32+
}

0 commit comments

Comments
 (0)