Skip to content

Commit 9d3dbe9

Browse files
committed
Finalize CLI
1 parent 4170ec3 commit 9d3dbe9

File tree

6 files changed

+142
-44
lines changed

6 files changed

+142
-44
lines changed

Diff for: .idea/misc.xml

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: src/main/java/airsquared/blobsaver/app/Analytics.java

+8
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ public static void exitRecovery() {
6666
collect("/exit-recovery");
6767
}
6868

69+
public static void importPrefs() {
70+
collect("/prefs/import");
71+
}
72+
73+
public static void exportPrefs() {
74+
collect("/prefs/export");
75+
}
76+
6977
public static void resetPrefs() {
7078
collect("/clear-app-data");
7179
}

Diff for: src/main/java/airsquared/blobsaver/app/CLI.java

+104-31
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
package airsquared.blobsaver.app;
2020

21+
import airsquared.blobsaver.app.LibimobiledeviceUtil.LibimobiledeviceException;
2122
import picocli.CommandLine;
2223
import picocli.CommandLine.*;
2324
import picocli.CommandLine.Model.ArgSpec;
@@ -27,20 +28,20 @@
2728
import java.io.File;
2829
import java.io.IOException;
2930
import java.io.PrintWriter;
31+
import java.util.Base64;
3032
import java.util.HashSet;
3133
import java.util.Scanner;
3234
import java.util.concurrent.Callable;
3335
import java.util.prefs.InvalidPreferencesFormatException;
3436
import java.util.stream.Collectors;
3537

36-
@Command(name = "blobsaver", versionProvider = CLI.VersionProvider.class, header = CLI.warning,
38+
@Command(name = "blobsaver", versionProvider = CLI.VersionProvider.class, mixinStandardHelpOptions = true,
39+
sortOptions = false, usageHelpAutoWidth = true, sortSynopsis = false, abbreviateSynopsis = true,
3740
optionListHeading = " You can separate options and their parameters with either a space or '='.%n",
38-
mixinStandardHelpOptions = true, sortOptions = false, usageHelpAutoWidth = true, sortSynopsis = false,
39-
abbreviateSynopsis = true, synopsisSubcommandLabel = "")
41+
commandListHeading = "Commands:%n See @|bold,white blobsaver help [COMMAND]|@ for more information about each command.%n%n",
42+
subcommands = HelpCommand.class)
4043
public class CLI implements Callable<Void> {
4144

42-
public static final String warning = "Warning: blobsaver's CLI is in alpha. Commands, options, and exit codes may change at any time.%n";
43-
4445
@Option(names = {"-s", "--save-blobs"})
4546
boolean saveBlobs;
4647

@@ -50,10 +51,12 @@ public class CLI implements Callable<Void> {
5051
@Option(names = "--remove-device", paramLabel = "<Saved Device>", description = "Remove a saved device.")
5152
Prefs.SavedDevice removeDevice;
5253

53-
@Option(names = "--enable-background", paramLabel = "<Saved Device>", description = "Enable background saving for a device.%nUse '--start-background-service' once devices are added.")
54+
@Option(names = "--enable-background", paramLabel = "<Saved Device>",
55+
description = "Enable background saving for a device.%nUse '--start-background-service' once devices are added.")
5456
Prefs.SavedDevice enableBackground;
5557

56-
@Option(names = "--disable-background", paramLabel = "<Saved Device>", description = "Disable background saving for a device.")
58+
@Option(names = "--disable-background", paramLabel = "<Saved Device>",
59+
description = "Disable background saving for a device.")
5760
Prefs.SavedDevice disableBackground;
5861

5962
@ArgGroup
@@ -68,7 +71,7 @@ static class BackgroundControls {
6871
boolean backgroundAutosave;
6972
}
7073

71-
@Option(names = "--export", paramLabel = "<path>", description = "Export saved devices in XML format to the directory.")
74+
@Option(names = "--export", paramLabel = "<path>", description = "Export saved devices in XML format to a directory.")
7275
File exportPath;
7376

7477
@Option(names = "--import", paramLabel = "<path>", description = "Import saved devices from a blobsaver XML file.")
@@ -94,7 +97,7 @@ static class BackgroundControls {
9497
File savePath;
9598

9699
@ArgGroup
97-
Version version;
100+
Version version = new Version();
98101
static class Version {
99102
@Option(names = "--ios-version", paramLabel = "<version>")
100103
String manualVersion;
@@ -111,15 +114,15 @@ static class Version {
111114
public Void call() throws TSS.TSSException, IOException, InvalidPreferencesFormatException {
112115
if (importPath != null) {
113116
Prefs.importXML(importPath);
114-
System.out.println("Successfully imported saved devices.");
117+
System.out.println(success("Successfully imported saved devices."));
115118
}
116119
if (saveBlobs) {
117120
checkArgs("identifier", "ecid", "save-path");
118121
var tss = new TSS.Builder()
119122
.setDevice(device).setEcid(ecid).setSavePath(savePath.getCanonicalPath()).setBoardConfig(boardConfig)
120123
.setManualVersion(version.manualVersion).setManualIpswURL(version.manualIpswURL).setApnonce(apnonce)
121124
.setGenerator(generator).setIncludeBetas(version.includeBetas).build();
122-
System.out.println(tss.call());
125+
System.out.println(success("\n" + tss.call()));
123126
}
124127
if (removeDevice != null) {
125128
removeDevice.delete();
@@ -130,15 +133,15 @@ public Void call() throws TSS.TSSException, IOException, InvalidPreferencesForma
130133
var saved = new Prefs.SavedDeviceBuilder(saveDevice)
131134
.setIdentifier(device).setEcid(ecid).setSavePath(savePath.getCanonicalPath()).setBoardConfig(boardConfig)
132135
.setApnonce(apnonce).setGenerator(generator).setIncludeBetas(version.includeBetas).save();
133-
System.out.println("Saved " + saved + ".");
136+
System.out.println(success("Saved " + saved + "."));
134137
}
135138
if (enableBackground != null) {
136139
if (!saveBlobs) {
137140
System.out.println("Testing device\n");
138141
Background.saveBlobs(enableBackground);
139142
}
140143
enableBackground.setBackground(true);
141-
System.out.println("Enabled background for " + enableBackground + ".");
144+
System.out.println(success("\nEnabled background for " + enableBackground + "."));
142145
}
143146
if (disableBackground != null) {
144147
disableBackground.setBackground(false);
@@ -147,7 +150,7 @@ public Void call() throws TSS.TSSException, IOException, InvalidPreferencesForma
147150
if (backgroundControls.startBackground) {
148151
Background.startBackground();
149152
if (Background.isBackgroundEnabled()) {
150-
System.out.println("A background saving task has been scheduled.");
153+
System.out.println(success("A background saving task has been scheduled."));
151154
} else {
152155
throw new ExecutionException(spec.commandLine(), "Error: Unable to enable background saving.");
153156
}
@@ -162,12 +165,11 @@ public Void call() throws TSS.TSSException, IOException, InvalidPreferencesForma
162165
exportPath = new File(exportPath, "blobsaver.xml");
163166
}
164167
Prefs.export(exportPath);
165-
System.out.println("Successfully exported saved devices.");
168+
System.out.println(success("Successfully exported saved devices."));
166169
}
167170
return null;
168171
}
169172

170-
@SuppressWarnings("unused")
171173
@Command(name = "clear-app-data", description = "Remove all of blobsaver's data including saved devices.")
172174
void clearAppData() {
173175
System.out.print("Are you sure you would like to permanently clear all blobsaver data? ");
@@ -178,31 +180,87 @@ void clearAppData() {
178180
}
179181
}
180182

181-
@SuppressWarnings("unused")
182-
@Command(name = "donate", description = "https://www.paypal.me/airsqrd")
183+
@Command(description = "Help support me and the development of this application! (I'm only a student)")
183184
void donate() {
184-
System.out.println("You can donate at https://www.paypal.me/airsqrd or with GitHub Sponsors at https://github.com/sponsors/airsquared.");
185+
System.out.println("""
186+
You can donate with GitHub Sponsors at
187+
https://github.com/sponsors/airsquared
188+
or with PayPal at
189+
https://www.paypal.me/airsqrd.
190+
Thank you!""");
191+
}
192+
193+
@Command(name = "read-info", description = "Reads ECID, identifier, board configuration, device type, and device name.")
194+
void readInfo() throws LibimobiledeviceException {
195+
long ecid = LibimobiledeviceUtil.getECID();
196+
System.out.println("ECID (hex): " + Long.toHexString(ecid).toUpperCase());
197+
System.out.println("ECID (dec): " + ecid);
198+
String identifier = LibimobiledeviceUtil.getDeviceModelIdentifier();
199+
System.out.println("Identifier: " + identifier);
200+
System.out.println("Board Configuration: " + LibimobiledeviceUtil.getBoardConfig()
201+
+ (Devices.doesRequireBoardConfig(identifier) ? " (Required)" : " (Not Required)"));
202+
System.out.println("Device Type: " + Devices.getDeviceType(identifier));
203+
System.out.println("Device Name: " + Devices.identifierToModel(identifier));
204+
205+
Analytics.readInfo();
206+
}
207+
208+
@Command(name = "read-apnonce", description = "Enters recovery mode to read APNonce, and freezes it if needed.")
209+
void readAPNonce(@Option(names = "--force-new", description = "Generate a new APNonce, even if it is already frozen.") boolean forceNew) throws LibimobiledeviceException {
210+
var task = new LibimobiledeviceUtil.GetApnonceTask(forceNew) {
211+
@Override
212+
protected void updateMessage(String message) {
213+
System.out.println(message);
214+
}
215+
};
216+
task.call();
217+
System.out.println("APNonce: " + task.getApnonceResult());
218+
System.out.println("Generator: " + task.getGeneratorResult());
219+
}
220+
221+
@Command(name = "exit-recovery")
222+
void exitRecovery() throws LibimobiledeviceException {
223+
LibimobiledeviceUtil.exitRecovery();
224+
Analytics.exitRecovery();
225+
}
226+
227+
@Command(description = "Read a key from lockdownd.", showDefaultValues = true)
228+
void read(@Parameters(paramLabel = "<key>") String key,
229+
@Parameters(paramLabel = "[output-type]", defaultValue = "xml", description = "Can be any of [xml, string, integer, base64]") PlistOutputType type) throws LibimobiledeviceException {
230+
var plist = LibimobiledeviceUtil.getLockdownValuePlist(key);
231+
System.out.println(switch (type) {
232+
case XML -> LibimobiledeviceUtil.plistToXml(plist);
233+
case STRING -> LibimobiledeviceUtil.getPlistString(plist);
234+
case INTEGER -> LibimobiledeviceUtil.getPlistLong(plist);
235+
case BASE64 -> Base64.getEncoder().encodeToString(LibimobiledeviceUtil.plistDataBytes(plist));
236+
});
185237
}
238+
enum PlistOutputType {XML, STRING, INTEGER, BASE64}
186239

187240
public static class VersionProvider implements IVersionProvider {
188241
@Override
189242
public String[] getVersion() {
190-
String[] output = {CLI.warning, "blobsaver " + Main.appVersion, Main.copyright, null};
243+
String[] output = {"blobsaver " + Main.appVersion, Main.copyright, "Licence: GNU GPL v3.0-only", null,
244+
"%nHomepage: https://github.com/airsquared/blobsaver"};
191245
try {
192246
var newVersion = Utils.LatestVersion.request();
193247
if (Main.appVersion.equals(newVersion.toString())) {
194-
output[3] = "You are on the latest version.";
248+
output[3] = "%nYou are on the latest version.";
195249
} else {
196-
output[3] = "New Update Available: " + newVersion + ". Update at%n https://github.com/airsquared/blobsaver/releases";
250+
output[3] = "%nNew Update Available: " + newVersion + ". Update using your package manager or at%n https://github.com/airsquared/blobsaver/releases";
197251
}
198252
} catch (Exception e) {
199-
output[3] = "Unable to check for updates.";
253+
output[3] = "%nUnable to check for updates.";
200254
}
201255

202256
return output;
203257
}
204258
}
205259

260+
private static String success(String s) {
261+
return Help.Ansi.AUTO.string("@|bold,green " + s + "|@");
262+
}
263+
206264
private void checkArgs(String... names) {
207265
var missing = new HashSet<ArgSpec>();
208266
for (String name : names) {
@@ -223,10 +281,11 @@ private static Prefs.SavedDevice savedDeviceConverter(String name) {
223281
.findAny().orElseThrow(() -> new TypeConversionException("Must be one of " + Prefs.getSavedDevices() + "\n"));
224282
}
225283

226-
public static int handleExecutionException(Exception ex, CommandLine cmd, ParseResult parseResult) throws Exception {
284+
private static int handleExecutionException(Exception ex, CommandLine cmd, ParseResult parseResult) throws Exception {
227285
boolean messageOnly = ex instanceof ExecutionException
228286
// if either the exception is not reportable or there is a tssLog present
229-
|| ex instanceof TSS.TSSException e && (!e.isReportable || e.tssLog != null);
287+
|| ex instanceof TSS.TSSException e && (!e.isReportable || e.tssLog != null)
288+
|| ex instanceof LibimobiledeviceException;
230289
if (messageOnly) {
231290
cmd.getErr().println(cmd.getColorScheme().errorText(ex.getMessage()));
232291

@@ -237,8 +296,10 @@ public static int handleExecutionException(Exception ex, CommandLine cmd, ParseR
237296
throw ex;
238297
}
239298

240-
public static int handleParseException(ParameterException ex, String[] args) {
299+
private static int handleParameterException(ParameterException ex, String[] args) {
241300
CommandLine cmd = ex.getCommandLine();
301+
CommandSpec spec = cmd.getCommandSpec();
302+
boolean isRootCommand = spec == spec.root();
242303
PrintWriter err = cmd.getErr();
243304

244305
// if tracing at DEBUG level, show the location of the issue
@@ -248,10 +309,12 @@ public static int handleParseException(ParameterException ex, String[] args) {
248309

249310
err.println(cmd.getColorScheme().errorText(ex.getMessage())); // bold red
250311
UnmatchedArgumentException.printSuggestions(ex, err);
251-
err.print(cmd.getHelp().fullSynopsis());
252-
253-
CommandSpec spec = cmd.getCommandSpec();
254-
err.printf("Try '%s --help' for more information.%n", spec.qualifiedName());
312+
if (isRootCommand) {
313+
err.print(cmd.getHelp().fullSynopsis());
314+
err.printf("Try '%s help' for more information.%n", spec.name());
315+
} else {
316+
cmd.usage(err); // print full help
317+
}
255318

256319
return cmd.getExitCodeExceptionMapper() != null
257320
? cmd.getExitCodeExceptionMapper().getExitCode(ex)
@@ -262,10 +325,20 @@ public static int handleParseException(ParameterException ex, String[] args) {
262325
* @return the exit code
263326
*/
264327
public static int launch(String... args) {
328+
Analytics.startup();
265329
var c = new CommandLine(new CLI())
330+
.setCaseInsensitiveEnumValuesAllowed(true)
266331
.setExecutionExceptionHandler(CLI::handleExecutionException)
267-
.setParameterExceptionHandler(CLI::handleParseException)
332+
.setParameterExceptionHandler(CLI::handleParameterException)
268333
.registerConverter(Prefs.SavedDevice.class, CLI::savedDeviceConverter);
334+
if (args.length == 0) { // happens when environment variable $BLOBSAVER_CLI_ONLY is set to some value
335+
args = new String[]{"help"};
336+
}
269337
return c.execute(args);
270338
}
339+
340+
/**
341+
* Private Constructor; Use {@link CLI#launch(String...)} instead
342+
*/
343+
private CLI() {}
271344
}

0 commit comments

Comments
 (0)