Skip to content

Commit 90e8752

Browse files
authored
feat: add configuration support (#5)
1 parent f5a8793 commit 90e8752

26 files changed

+503
-144
lines changed

.swift-format

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"version" : 1,
3+
"indentation" : {
4+
"spaces" : 4
5+
},
6+
"tabWidth" : 4,
7+
"fileScopedDeclarationPrivacy" : {
8+
"accessLevel" : "private"
9+
},
10+
"spacesAroundRangeFormationOperators" : false,
11+
"indentConditionalCompilationBlocks" : false,
12+
"indentSwitchCaseLabels" : false,
13+
"lineBreakAroundMultilineExpressionChainComponents" : false,
14+
"lineBreakBeforeControlFlowKeywords" : false,
15+
"lineBreakBeforeEachArgument" : true,
16+
"lineBreakBeforeEachGenericRequirement" : true,
17+
"lineLength" : 120,
18+
"maximumBlankLines" : 1,
19+
"respectsExistingLineBreaks" : true,
20+
"prioritizeKeepingFunctionOutputTogether" : true,
21+
"rules" : {
22+
"AllPublicDeclarationsHaveDocumentation" : false,
23+
"AlwaysUseLiteralForEmptyCollectionInit" : false,
24+
"AlwaysUseLowerCamelCase" : false,
25+
"AmbiguousTrailingClosureOverload" : true,
26+
"BeginDocumentationCommentWithOneLineSummary" : false,
27+
"DoNotUseSemicolons" : true,
28+
"DontRepeatTypeInStaticProperties" : true,
29+
"FileScopedDeclarationPrivacy" : true,
30+
"FullyIndirectEnum" : true,
31+
"GroupNumericLiterals" : true,
32+
"IdentifiersMustBeASCII" : true,
33+
"NeverForceUnwrap" : false,
34+
"NeverUseForceTry" : false,
35+
"NeverUseImplicitlyUnwrappedOptionals" : false,
36+
"NoAccessLevelOnExtensionDeclaration" : true,
37+
"NoAssignmentInExpressions" : true,
38+
"NoBlockComments" : true,
39+
"NoCasesWithOnlyFallthrough" : true,
40+
"NoEmptyTrailingClosureParentheses" : true,
41+
"NoLabelsInCasePatterns" : true,
42+
"NoLeadingUnderscores" : false,
43+
"NoParensAroundConditions" : true,
44+
"NoVoidReturnOnFunctionSignature" : true,
45+
"OmitExplicitReturns" : true,
46+
"OneCasePerLine" : true,
47+
"OneVariableDeclarationPerLine" : true,
48+
"OnlyOneTrailingClosureArgument" : true,
49+
"OrderedImports" : true,
50+
"ReplaceForEachWithForLoop" : true,
51+
"ReturnVoidInsteadOfEmptyTuple" : true,
52+
"UseEarlyExits" : false,
53+
"UseExplicitNilCheckInConditions" : false,
54+
"UseLetInEveryBoundCaseVariable" : false,
55+
"UseShorthandTypeNames" : true,
56+
"UseSingleLinePropertyGetter" : false,
57+
"UseSynthesizedInitializer" : false,
58+
"UseTripleSlashForDocumentationComments" : true,
59+
"UseWhereClausesInForLoops" : false,
60+
"ValidateDocumentationComments" : false
61+
}
62+
}

.swiftlint.yml

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

Package.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import PackageDescription
66
let package = Package(
77
name: "lcl-cli",
88
platforms: [
9-
.macOS(.v11)
9+
.macOS(.v13)
1010
],
1111
dependencies: [
12-
.package(url: "https://github.com/Local-Connectivity-Lab/lcl-ping.git", from: "1.0.3"),
12+
.package(url: "https://github.com/Local-Connectivity-Lab/lcl-ping.git", from: "1.0.4"),
1313
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.3"),
1414
.package(url: "https://github.com/scottrhoyt/SwiftyTextTable.git", from: "0.9.0"),
1515
.package(url: "https://github.com/Local-Connectivity-Lab/lcl-auth.git", branch: "main"),
@@ -32,8 +32,9 @@ let package = Package(
3232
.product(name: "NIOHTTP1", package: "swift-nio"),
3333
.product(name: "NIOSSL", package: "swift-nio-ssl"),
3434
.product(name: "LCLSpeedtest", package: "lcl-speedtest"),
35-
.product(name: "ANSITerminal", package: "ANSITerminal")
35+
.product(name: "ANSITerminal", package: "ANSITerminal"),
3636
],
37-
path: "Sources")
37+
path: "Sources"
38+
)
3839
]
3940
)

Sources/Command/CellularSiteCommand.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
// SPDX-License-Identifier: Apache-2.0
1111
//
1212

13-
import Foundation
1413
import ArgumentParser
14+
import Foundation
1515

1616
extension LCLCLI {
1717
struct CelluarSiteCommand: AsyncParsableCommand {
@@ -20,8 +20,9 @@ extension LCLCLI {
2020
abstract: "Get info on SCN cellular sites"
2121
)
2222
func run() async throws {
23-
let result: Result<[CellularSite]?, CLIError> = await NetworkingAPI
24-
.get(from: NetworkingAPI.Endpoint.site.url)
23+
let result: Result<[CellularSite]?, CLIError> =
24+
await NetworkingAPI
25+
.get(from: NetworkingAPI.Endpoint.site.url)
2526
switch result {
2627
case .failure(let error):
2728
throw error

Sources/Command/LCLCLI.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@
1111
//
1212

1313
import ArgumentParser
14-
import LCLPing
15-
import Foundation
1614
import Dispatch
15+
import Foundation
1716
import LCLAuth
17+
import LCLPing
1818

1919
#if canImport(Darwin)
20-
import Darwin // Apple platforms
20+
import Darwin // Apple platforms
2121
#elseif canImport(Glibc)
22-
import Glibc // GlibC Linux platforms
22+
import Glibc // GlibC Linux platforms
2323
#endif
2424

2525
@main
@@ -33,7 +33,7 @@ struct LCLCLI: AsyncParsableCommand {
3333
SpeedTestCommand.self,
3434
MeasureCommand.self,
3535
NetworkInterfaceCommand.self,
36-
CelluarSiteCommand.self
36+
CelluarSiteCommand.self,
3737
]
3838
)
3939
}

Sources/Command/Measure.swift

Lines changed: 127 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@
1010
// SPDX-License-Identifier: Apache-2.0
1111
//
1212

13-
import Foundation
13+
import ANSITerminal
1414
import ArgumentParser
15-
import LCLPing
16-
import LCLAuth
1715
import Crypto
16+
import Foundation
17+
import LCLAuth
18+
import LCLPing
1819
import LCLSpeedtest
1920

2021
extension LCLCLI {
@@ -23,21 +24,43 @@ extension LCLCLI {
2324
@Option(name: .long, help: "Specify the device name to which the data will be sent.")
2425
var deviceName: String?
2526

26-
@Option(name: .shortAndLong, help: "Show datapoint on SCN's public visualization. Your contribution will help others better understand our coverage.")
27+
@Option(
28+
name: .shortAndLong,
29+
help:
30+
"Show datapoint on SCN's public visualization. Your contribution will help others better understand our coverage."
31+
)
2732
var showData: Bool = false
2833

34+
@Option(
35+
name: .shortAndLong,
36+
help:
37+
"The path to the configuration file that will be used to configure the measurement process"
38+
)
39+
var configurationFile: String?
40+
2941
static let configuration = CommandConfiguration(
3042
commandName: "measure",
3143
abstract: "Run SCN test suite and optionally report the measurement result to SCN."
3244
)
3345

3446
func run() async throws {
35-
let encoder: JSONEncoder = JSONEncoder()
36-
encoder.outputFormatting = .sortedKeys
47+
var configuration: Configuration? = nil
48+
if let configurationFile = configurationFile {
49+
let configURL = URL(fileURLWithPath: configurationFile)
50+
guard let configObject = try? Data(contentsOf: configURL) else {
51+
throw CLIError.invalidConfiguration("Corrupted config file.")
52+
}
53+
let decoder = JSONDecoder()
54+
guard let config = try? decoder.decode(Configuration.self, from: configObject) else {
55+
throw CLIError.invalidConfiguration("Invalid config file format.")
56+
}
57+
configuration = config
58+
}
3759

38-
// TODO: prompted picker if the location option is not set
3960
var sites: [CellularSite]
40-
let result: Result<[CellularSite]?, CLIError> = await NetworkingAPI.get(from: NetworkingAPI.Endpoint.site.url)
61+
let result: Result<[CellularSite]?, CLIError> = await NetworkingAPI.get(
62+
from: NetworkingAPI.Endpoint.site.url
63+
)
4164
switch result {
4265
case .failure(let error):
4366

@@ -46,11 +69,14 @@ extension LCLCLI {
4669
if let s = cs {
4770
sites = s
4871
} else {
49-
throw CLIError.failedToLoadContent("No cellular site is available. Please check your internet connection or talk to the SCN administrator.")
72+
throw CLIError.failedToLoadContent(
73+
"No cellular site is available. Please check your internet connection or talk to the SCN administrator."
74+
)
5075
}
51-
5276
}
53-
var picker = Picker<CellularSite>(title: "Choose the cellular site you are currently at.", options: sites)
77+
78+
let encoder: JSONEncoder = JSONEncoder()
79+
encoder.outputFormatting = .sortedKeys
5480

5581
let homeURL = FileIO.default.home.appendingPathComponent(Constants.cliDirectory)
5682
let skURL = homeURL.appendingPathComponent("sk")
@@ -62,15 +88,26 @@ extension LCLCLI {
6288
let sigData = try loadData(sigURL)
6389
let rData = try loadData(rURL)
6490
let hpkrData = try loadData(hpkrURL)
65-
let validationResultJSON = try encoder.encode(ValidationResult(R: rData, skT: skData, hPKR: hpkrData))
91+
let validationResultJSON = try encoder.encode(
92+
ValidationResult(R: rData, skT: skData, hPKR: hpkrData)
93+
)
6694

6795
let ecPrivateKey = try ECDSA.deserializePrivateKey(raw: skData)
6896

69-
guard try ECDSA.verify(message: validationResultJSON, signature: sigData, publicKey: ecPrivateKey.publicKey) else {
97+
guard
98+
try ECDSA.verify(
99+
message: validationResultJSON,
100+
signature: sigData,
101+
publicKey: ecPrivateKey.publicKey
102+
)
103+
else {
70104
throw CLIError.contentCorrupted
71105
}
72106

73-
let pingConfig = try ICMPPingClient.Configuration(endpoint: .ipv4("google.com", 0), deviceName: deviceName)
107+
let pingConfig = try ICMPPingClient.Configuration(
108+
endpoint: .ipv4("google.com", 0),
109+
deviceName: deviceName
110+
)
74111
let outputFormats: Set<OutputFormat> = [.default]
75112

76113
let client = ICMPPingClient(configuration: pingConfig)
@@ -80,30 +117,77 @@ extension LCLCLI {
80117
let stopSignal = DispatchSource.makeSignalSource(signal: SIGINT, queue: .main)
81118
stopSignal.setEventHandler {
82119
print("Exit from SCN Measurement Test")
120+
cursorOn()
83121
client.cancel()
122+
print("Ping test cancelled")
84123
speedTest.stop()
85-
return
124+
print("Speed test cancelled")
125+
LCLCLI.MeasureCommand.exit()
86126
}
87127

88128
stopSignal.resume()
89129

90-
guard let selectedSite = picker.pick() else {
91-
throw CLIError.noCellularSiteSelected
130+
var selectedSite: CellularSite
131+
132+
if configuration == nil {
133+
var picker = Picker<CellularSite>(
134+
title: "Choose the cellular site you are currently at.",
135+
options: sites
136+
)
137+
guard let ss = picker.pick() else {
138+
throw CLIError.noCellularSiteSelected
139+
}
140+
selectedSite = ss
141+
} else {
142+
print("Using configuration \(configurationFile ?? "")")
143+
#if DEBUG
144+
print(configuration ?? "Empty configuration")
145+
#endif
146+
let siteMap = Dictionary(uniqueKeysWithValues: sites.map { ($0.name, $0) })
147+
guard let cellSiteName = configuration?.cellSiteName else {
148+
throw CLIError.invalidConfiguration("Missing cellular site name.")
149+
}
150+
151+
guard let ss = siteMap[cellSiteName] else {
152+
throw CLIError.invalidConfiguration("Invalid cellular site name.")
153+
}
154+
selectedSite = ss
92155
}
93156

94157
let deviceId = UUID().uuidString
95158

96159
let summary = try await client.start().get()
97160

98161
let speedTestResults = try await speedTest.run(deviceName: deviceName)
99-
let downloadMeasurement = computeLatencyAndRetransmission(speedTestResults.downloadTCPMeasurement, for: .download)
100-
let uploadMeasurement = computeLatencyAndRetransmission(speedTestResults.uploadTCPMeasurement, for: .upload)
101-
102-
let downloadSummary = prepareSpeedTestSummary(data: speedTestResults.downloadSpeed, tcpInfos: speedTestResults.downloadTCPMeasurement, for: .download, unit: .Mbps)
103-
let uploadSummary = prepareSpeedTestSummary(data: speedTestResults.uploadSpeed, tcpInfos: speedTestResults.uploadTCPMeasurement, for: .upload, unit: .Mbps)
162+
let downloadMeasurement = computeLatencyAndRetransmission(
163+
speedTestResults.downloadTCPMeasurement,
164+
for: .download
165+
)
166+
let uploadMeasurement = computeLatencyAndRetransmission(
167+
speedTestResults.uploadTCPMeasurement,
168+
for: .upload
169+
)
170+
171+
let downloadSummary = prepareSpeedTestSummary(
172+
data: speedTestResults.downloadSpeed,
173+
tcpInfos: speedTestResults.downloadTCPMeasurement,
174+
for: .download,
175+
unit: .Mbps
176+
)
177+
let uploadSummary = prepareSpeedTestSummary(
178+
data: speedTestResults.uploadSpeed,
179+
tcpInfos: speedTestResults.uploadTCPMeasurement,
180+
for: .upload,
181+
unit: .Mbps
182+
)
104183

105184
generatePingSummary(summary, for: .icmp, formats: outputFormats)
106-
generateSpeedTestSummary(downloadSummary, kind: .download, formats: outputFormats, unit: .Mbps)
185+
generateSpeedTestSummary(
186+
downloadSummary,
187+
kind: .download,
188+
formats: outputFormats,
189+
unit: .Mbps
190+
)
107191
generateSpeedTestSummary(uploadSummary, kind: .upload, formats: outputFormats, unit: .Mbps)
108192

109193
// MARK: Upload test results to the server
@@ -122,11 +206,28 @@ extension LCLCLI {
122206
jitter: (downloadMeasurement.variance + uploadMeasurement.variance) / 2
123207
)
124208
let serialized = try encoder.encode(report)
125-
let sig_m = try ECDSA.sign(message: serialized, privateKey: ECDSA.deserializePrivateKey(raw: skData))
126-
let measurementReport = MeasurementReportModel(sigmaM: sig_m.hex, hPKR: hpkrData.hex, M: serialized.hex, showData: showData)
209+
let sig_m = try ECDSA.sign(
210+
message: serialized,
211+
privateKey: ECDSA.deserializePrivateKey(raw: skData)
212+
)
213+
let measurementReport = MeasurementReportModel(
214+
sigmaM: sig_m.hex,
215+
hPKR: hpkrData.hex,
216+
M: serialized.hex,
217+
showData: showData
218+
)
127219

128220
let reportToSent = try encoder.encode(measurementReport)
129-
let result = await NetworkingAPI.send(to: NetworkingAPI.Endpoint.report.url, using: reportToSent)
221+
222+
#if DEBUG
223+
print(measurementReport)
224+
print(String(data: reportToSent, encoding: .utf8) ?? "Unable to convert data to string")
225+
print("DONE")
226+
#endif
227+
let result = await NetworkingAPI.send(
228+
to: NetworkingAPI.Endpoint.report.url,
229+
using: reportToSent
230+
)
130231
switch result {
131232
case .success:
132233
print("Data reported successfully.")

0 commit comments

Comments
 (0)