Skip to content

Commit eb472e7

Browse files
authored
CLI (#543)
Add a WIP CLI
1 parent c557671 commit eb472e7

File tree

7 files changed

+277
-8
lines changed

7 files changed

+277
-8
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ dependencies {
5757
}
5858
implementation 'net.java.dev.jna:jna-jpms:5.13.0'
5959
implementation 'org.apache.commons:commons-compress:1.22'
60+
implementation 'info.picocli:picocli:4.7.0'
6061

6162
testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2'
6263
testImplementation group: 'org.testfx', name: 'openjfx-monocle', version: 'jdk-12.0.1+2'

src/main/java/airsquared/blobsaver/app/Analytics.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ private static String getBaseUrl() {
8686
}
8787

8888
private static String getUUID() {
89+
if (System.getenv("GITHUB_ACTIONS").equals("true")) {
90+
return "GITHUB_ACTIONS";
91+
}
8992
if (Prefs.getAnalyticsUUID() == null) {
9093
Prefs.setAnalyticsUUID(UUID.randomUUID().toString());
9194
}

src/main/java/airsquared/blobsaver/app/Background.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -260,13 +260,15 @@ private static boolean outputMatches(Predicate<String> predicate, String... args
260260
}
261261

262262
public static void saveAllBackgroundBlobs() {
263-
Prefs.getBackgroundDevices().forEach(Background::saveBlobs);
263+
Prefs.getBackgroundDevices().forEach(savedDevice -> {
264+
System.out.println("attempting to save for device " + savedDevice);
265+
266+
saveBlobs(savedDevice);
267+
});
264268
System.out.println("Done saving all background blobs");
265269
}
266270

267-
private static void saveBlobs(Prefs.SavedDevice savedDevice) {
268-
System.out.println("attempting to save for device " + savedDevice);
269-
271+
public static void saveBlobs(Prefs.SavedDevice savedDevice) {
270272
TSS.Builder builder = new TSS.Builder().setDevice(savedDevice.getIdentifier())
271273
.setEcid(savedDevice.getEcid()).setSavePath(savedDevice.getSavePath())
272274
.setIncludeBetas(savedDevice.doesIncludeBetas());
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
/*
2+
* Copyright (c) 2022 airsquared
3+
*
4+
* This file is part of blobsaver.
5+
*
6+
* blobsaver is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, version 3 of the License.
9+
*
10+
* blobsaver is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with blobsaver. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
package airsquared.blobsaver.app;
20+
21+
import picocli.CommandLine;
22+
import picocli.CommandLine.*;
23+
import picocli.CommandLine.Model.ArgSpec;
24+
import picocli.CommandLine.Model.CommandSpec;
25+
import picocli.CommandLine.Model.OptionSpec;
26+
27+
import java.io.File;
28+
import java.io.IOException;
29+
import java.io.PrintWriter;
30+
import java.util.HashSet;
31+
import java.util.concurrent.Callable;
32+
import java.util.prefs.InvalidPreferencesFormatException;
33+
import java.util.stream.Collectors;
34+
35+
@Command(name = "blobsaver", version = {CLI.warning, Main.appVersion}, header = CLI.warning,
36+
optionListHeading = " You can separate options and their parameters with either a space or '='.%n",
37+
mixinStandardHelpOptions = true, sortOptions = false, sortSynopsis = false, usageHelpAutoWidth = true, abbreviateSynopsis = true)
38+
public class CLI implements Callable<Void> {
39+
40+
public static final String warning = "Warning: blobsaver's CLI is in alpha. Commands, options, and exit codes may change at any time.%n";
41+
42+
@Option(names = {"-s", "--save-blobs"})
43+
boolean saveBlobs;
44+
45+
@Option(names = "--save-device", paramLabel = "<name>", description = "Create a new saved device.")
46+
String saveDevice;
47+
48+
@Option(names = "--remove-device", paramLabel = "<Saved Device>", description = "Remove a saved device.")
49+
Prefs.SavedDevice removeDevice;
50+
51+
@Option(names = "--enable-background", paramLabel = "<Saved Device>", description = "Enable background saving for a device.%nUse '--start-background-service' once devices are added.")
52+
Prefs.SavedDevice enableBackground;
53+
54+
@Option(names = "--disable-background", paramLabel = "<Saved Device>", description = "Disable background saving for a device.")
55+
Prefs.SavedDevice disableBackground;
56+
57+
@ArgGroup
58+
BackgroundControls backgroundControls;
59+
static class BackgroundControls {
60+
@Option(names = "--start-background-service", description = "Register background saving service with the OS.")
61+
boolean startBackground;
62+
@Option(names = "--stop-background-service", description = "Deregister background saving service from the OS.")
63+
boolean stopBackground;
64+
65+
@Option(names = "--background-autosave", description = "Save blobs for all devices configured to save in background. All other options have no effect on this option. Useful in scripts.")
66+
boolean backgroundAutosave;
67+
}
68+
69+
@Option(names = "--export", paramLabel = "<path>", description = "Export saved devices in XML format to the directory.")
70+
File exportPath;
71+
72+
@Option(names = "--import", paramLabel = "<path>", description = "Import saved devices from a blobsaver XML file.")
73+
File importPath;
74+
75+
@Option(names = "--identifier")
76+
String device;
77+
@Option(names = "--ecid")
78+
String ecid;
79+
80+
@Option(names = "--boardconfig")
81+
String boardConfig;
82+
83+
@Option(names = "--apnonce")
84+
String apnonce;
85+
86+
@Option(names = "--generator")
87+
String generator;
88+
89+
@Option(names = "--save-path", paramLabel = "<path>",
90+
description = "Directory to save blobs in. Can use the following variables: " +
91+
"$${DeviceIdentifier}, $${BoardConfig}, $${APNonce}, $${Generator}, $${DeviceModel}, $${ECID}, $${FullVersionString}, $${BuildID}, and $${MajorVersion}.")
92+
File savePath;
93+
94+
@ArgGroup
95+
Version version;
96+
static class Version {
97+
@Option(names = "--ios-version", paramLabel = "<version>")
98+
String manualVersion;
99+
@Option(names = "--ipsw-url", paramLabel = "<url>", description = "Either a URL to an IPSW file or a build manifest. Local 'file:' URLs are accepted.")
100+
String manualIpswURL;
101+
102+
@Option(names = {"-b", "--include-betas"})
103+
boolean includeBetas;
104+
}
105+
106+
@Spec CommandSpec spec;
107+
108+
@Override
109+
public Void call() throws TSS.TSSException, IOException, InvalidPreferencesFormatException {
110+
if (importPath != null) {
111+
Prefs.importXML(importPath);
112+
System.out.println("Successfully imported saved devices.");
113+
}
114+
if (saveBlobs) {
115+
checkArgs("identifier", "ecid", "save-path");
116+
var tss = new TSS.Builder()
117+
.setDevice(device).setEcid(ecid).setSavePath(savePath.getCanonicalPath()).setBoardConfig(boardConfig)
118+
.setManualVersion(version.manualVersion).setManualIpswURL(version.manualIpswURL).setApnonce(apnonce)
119+
.setGenerator(generator).setIncludeBetas(version.includeBetas).build();
120+
System.out.println(tss.call());
121+
}
122+
if (removeDevice != null) {
123+
removeDevice.delete();
124+
System.out.println("Deleted " + removeDevice + ".");
125+
}
126+
if (saveDevice != null) {
127+
checkArgs("ecid", "save-path", "identifier");
128+
var saved = new Prefs.SavedDeviceBuilder(saveDevice)
129+
.setIdentifier(device).setEcid(ecid).setSavePath(savePath.getCanonicalPath()).setBoardConfig(boardConfig)
130+
.setApnonce(apnonce).setGenerator(generator).setIncludeBetas(version.includeBetas).save();
131+
System.out.println("Saved " + saved + ".");
132+
}
133+
if (enableBackground != null) {
134+
if (!saveBlobs) {
135+
System.out.println("Testing device\n");
136+
Background.saveBlobs(enableBackground);
137+
}
138+
enableBackground.setBackground(true);
139+
System.out.println("Enabled background for " + enableBackground + ".");
140+
}
141+
if (disableBackground != null) {
142+
disableBackground.setBackground(false);
143+
System.out.println("Disabled background for " + enableBackground + ".");
144+
}
145+
if (backgroundControls.startBackground) {
146+
Background.startBackground();
147+
if (Background.isBackgroundEnabled()) {
148+
System.out.println("A background saving task has been scheduled.");
149+
} else {
150+
throw new ExecutionException(spec.commandLine(), "Error: Unable to enable background saving.");
151+
}
152+
} else if (backgroundControls.stopBackground) {
153+
Background.stopBackground();
154+
} else if (backgroundControls.backgroundAutosave) {
155+
Background.saveAllBackgroundBlobs();
156+
}
157+
if (exportPath != null) {
158+
Prefs.export(exportPath);
159+
System.out.println("Successfully exported saved devices.");
160+
}
161+
return null;
162+
}
163+
164+
private void checkArgs(String... names) {
165+
var missing = new HashSet<ArgSpec>();
166+
for (String name : names) {
167+
if (!spec.commandLine().getParseResult().hasMatchedOption(name)) {
168+
missing.add(spec.findOption(name));
169+
}
170+
}
171+
if (!missing.isEmpty()) {
172+
String miss = missing.stream().map(OptionSpec.class::cast)
173+
.map(OptionSpec::longestName).collect(Collectors.joining(", "));
174+
throw new MissingParameterException(spec.commandLine(), missing, "Missing required options: " + miss);
175+
}
176+
}
177+
178+
private static Prefs.SavedDevice savedDeviceConverter(String name) {
179+
return Prefs.getSavedDevices().stream()
180+
.filter(savedDevice -> savedDevice.getName().equalsIgnoreCase(name))
181+
.findAny().orElseThrow(() -> new TypeConversionException("Must be one of " + Prefs.getSavedDevices() + "\n"));
182+
}
183+
184+
public static int handleExecutionException(Exception ex, CommandLine cmd, ParseResult parseResult) throws Exception {
185+
boolean messageOnly = ex instanceof ExecutionException
186+
// if either the exception is not reportable or there is a tssLog present
187+
|| ex instanceof TSS.TSSException e && (!e.isReportable || e.tssLog != null);
188+
if (messageOnly) {
189+
cmd.getErr().println(cmd.getColorScheme().errorText(ex.getMessage()));
190+
191+
return cmd.getExitCodeExceptionMapper() != null
192+
? cmd.getExitCodeExceptionMapper().getExitCode(ex)
193+
: cmd.getCommandSpec().exitCodeOnExecutionException();
194+
}
195+
throw ex;
196+
}
197+
198+
public static int handleParseException(ParameterException ex, String[] args) {
199+
CommandLine cmd = ex.getCommandLine();
200+
PrintWriter err = cmd.getErr();
201+
202+
// if tracing at DEBUG level, show the location of the issue
203+
if ("DEBUG".equalsIgnoreCase(System.getProperty("picocli.trace"))) {
204+
err.println(cmd.getColorScheme().stackTraceText(ex));
205+
}
206+
207+
err.println(cmd.getColorScheme().errorText(ex.getMessage())); // bold red
208+
UnmatchedArgumentException.printSuggestions(ex, err);
209+
err.print(cmd.getHelp().fullSynopsis());
210+
211+
CommandSpec spec = cmd.getCommandSpec();
212+
err.printf("Try '%s --help' for more information.%n", spec.qualifiedName());
213+
214+
return cmd.getExitCodeExceptionMapper() != null
215+
? cmd.getExitCodeExceptionMapper().getExitCode(ex)
216+
: spec.exitCodeOnInvalidInput();
217+
}
218+
219+
/**
220+
* @return the exit code
221+
*/
222+
public static int launch(String... args) {
223+
var c = new CommandLine(new CLI())
224+
.setExecutionExceptionHandler(CLI::handleExecutionException)
225+
.setParameterExceptionHandler(CLI::handleParseException)
226+
.registerConverter(Prefs.SavedDevice.class, CLI::savedDeviceConverter);
227+
return c.execute(args);
228+
}
229+
}

src/main/java/airsquared/blobsaver/app/Main.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,11 @@ public static void main(String[] args) {
6363
fixCertificateError();
6464
if (args.length == 1 && args[0].equals("--background-autosave")) {
6565
Background.saveAllBackgroundBlobs(); // don't unnecessarily initialize any FX
66-
return;
66+
} else if (args.length > 0) {
67+
System.exit(CLI.launch(args));
68+
} else {
69+
JavaFxApplication.launch(JavaFxApplication.class, args);
6770
}
68-
JavaFxApplication.launch(JavaFxApplication.class, args);
6971
}
7072

7173
public static void exit() {
@@ -83,7 +85,7 @@ static void setJNALibraryPath() {
8385
path = new File(jarDirectory, "lib/").getAbsolutePath();
8486
}
8587
System.setProperty("jna.library.path", path);
86-
System.out.println("path = " + path);
88+
// System.out.println("path = " + path);
8789
}
8890

8991
/**

src/main/java/module-info.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
open module airsquared.blobsaver {
22
requires java.prefs;
3+
requires java.net.http;
34
requires javafx.controls;
45
requires javafx.fxml;
56
requires jdk.crypto.ec; // needed for ssl support in jlink'd image
@@ -8,5 +9,5 @@
89
requires nsmenufx;
910
requires com.google.gson;
1011
requires org.apache.commons.compress;
11-
requires java.net.http;
12+
requires info.picocli;
1213
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright (c) 2022 airsquared
3+
*
4+
* This file is part of blobsaver.
5+
*
6+
* blobsaver is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, version 3 of the License.
9+
*
10+
* blobsaver is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with blobsaver. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
package airsquared.blobsaver.app;
20+
21+
import org.junit.jupiter.api.Test;
22+
23+
import static org.junit.jupiter.api.Assertions.assertEquals;
24+
class CLITest extends BlobsaverTest {
25+
26+
@Test
27+
public void test1() {
28+
int exitCode = CLI.launch("--help");
29+
assertEquals(0, exitCode, "Exit Code");
30+
}
31+
}

0 commit comments

Comments
 (0)