Skip to content

Commit 523703e

Browse files
committed
Use schtasks on Windows and systemd timers on Linux for background autosaving
1 parent 20500cf commit 523703e

File tree

7 files changed

+152
-55
lines changed

7 files changed

+152
-55
lines changed

build.gradle

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ idea.module.outputDir file("out/production/classes") // fix running via IntelliJ
3333
* REMEMBER: also update the version string in:
3434
* - Main.java
3535
*/
36-
version "3.0"
36+
version = "3.0"
37+
description = "A cross-platform GUI app for saving SHSH blobs"
3738
String appIdentifier = "airsquared.blobsaver.app"
3839
String copyright = "Copyright (c) 2020 airsquared"
3940
def os = DefaultNativePlatform.currentOperatingSystem
@@ -102,20 +103,19 @@ jlink {
102103
noConsole = true
103104
}
104105
jpackage {
105-
icon = "${projectDir}/${icon}"
106-
imageOptions = [ '--copyright', copyright, '--vendor', 'airsquared']
106+
imageOptions = [ '--copyright', copyright, '--description', description, '--vendor', 'airsquared']
107107

108108
if (os.isMacOsX()) {
109109
installerType = "dmg"
110110
imageOptions.addAll '--mac-package-identifier', appIdentifier
111-
icon = "dist/macos/Contents/Resources/blob.icns"
111+
icon = "${projectDir}/dist/macos/Contents/Resources/blob.icns"
112112
} else if (os.isWindows()) {
113113
installerOptions.addAll '--win-dir-chooser', '--win-menu', '--win-shortcut'
114-
icon = "dist/windows/blob.ico"
114+
icon = "${projectDir}/dist/windows/blob.ico"
115115
} else {
116116
installerOptions.addAll '--linux-shortcut', '--linux-menu-group', 'Utility;Archiving;Java',
117117
'--linux-rpm-license-type', 'GPLv3'
118-
icon = "src/main/resources/airsquared/blobsaver/app/blob_emoji.png"
118+
icon = "${projectDir}/src/main/resources/airsquared/blobsaver/app/blob_emoji.png"
119119
}
120120
}
121121
if (os.isMacOsX()) jpackageImage.doLast {
@@ -131,24 +131,36 @@ jlink {
131131
}
132132
} else if (os.isWindows()) jpackageImage.doLast {
133133
copy {
134-
from "${projectDir}/dist/windows/files" from "${buildDir}/jpackage/blobsaver/runtime/bin/zip.dll"
134+
from "${projectDir}/dist/windows/files", "${buildDir}/jpackage/blobsaver/runtime/bin/zip.dll"
135135
rename "LICENSE_windows", "LICENSE"
136136
rename "libraries_used_windows.txt", "libraries_used.txt"
137137
into "${buildDir}/jpackage/blobsaver"
138138
}
139139
} else jpackageImage.doLast {
140140
copy {
141-
from "${projectDir}/dist/linux" into "${buildDir}/jpackage/blobsaver/lib"
141+
from "${projectDir}/dist/linux", "${projectDir}/LICENSE", "${projectDir}/libraries_used.txt"
142+
into "${buildDir}/jpackage/blobsaver/lib"
142143
}
143144
}
144145

145146
}
146147

148+
task createLinuxTargz(type: Tar, dependsOn: jpackageImage) {
149+
archiveFileName = "blobsaver-linux.tar.gz"
150+
compression = Compression.GZIP
151+
destinationDirectory = file("${buildDir}/distributions/")
152+
from "${buildDir}/jpackage/blobsaver"
153+
154+
if (os.isLinux()) {
155+
assemble.dependsOn createLinuxTargz
156+
}
157+
}
158+
147159
task windowsInstaller(dependsOn: jpackageImage) { // requires inno setup to be installed
148160
doFirst {
149161
copy {
150162
from "${projectDir}/dist/windows/blobsaver.iss" into "${buildDir}/jpackage"
151-
filter(ReplaceTokens, tokens: [AppName: project.name, AppVersion: version, AppCopyright: copyright, AppMutex: appIdentifier])
163+
filter(ReplaceTokens, tokens: [AppName: project.name, AppVersion: version, AppCopyright: copyright])
152164
}
153165
exec {
154166
commandLine "iscc", "/Qp", "${buildDir}\\jpackage\\blobsaver.iss"

dist/windows/blobsaver.iss

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
#define MyAppURL "https://www.github.com/airsquared/blobsaver"
88
#define MyAppExeName "blobsaver.exe"
99
#define MyAppCopyright "@AppCopyright@"
10-
#define MyAppMutex "@AppMutex@"
1110

1211
[Setup]
1312
; NOTE: The value of AppId uniquely identifies this application.
@@ -22,7 +21,6 @@ AppPublisherURL={#MyAppURL}
2221
AppSupportURL={#MyAppURL}
2322
AppUpdatesURL={#MyAppURL}
2423
AppCopyright={#MyAppCopyright}
25-
AppMutex={#MyAppMutex}
2624
ArchitecturesAllowed=x64 arm64
2725
ArchitecturesInstallIn64BitMode=x64 arm64
2826
Uninstallable=not IsTaskSelected('portableMode')

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

Lines changed: 119 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -24,79 +24,153 @@
2424
import java.io.IOException;
2525
import java.io.InputStreamReader;
2626
import java.io.UncheckedIOException;
27-
import java.nio.charset.StandardCharsets;
2827
import java.nio.file.Files;
29-
import java.nio.file.Paths;
28+
import java.nio.file.Path;
29+
import java.text.SimpleDateFormat;
3030
import java.util.ArrayList;
3131
import java.util.Collections;
32+
import java.util.Date;
33+
import java.util.function.Predicate;
3234

3335
class Background {
3436

35-
private static final String backgroundLabel = "airsquared.blobsaver.app.BackgroundService";
36-
37-
private static final String plistFilePath = System.getProperty("user.home")
38-
+ "/Library/LaunchAgents/" + backgroundLabel + ".plist";
37+
private static final String backgroundLabel = "airsquared.blobsaver.BackgroundService";
38+
private static final Path plistFilePath = Path.of(System.getProperty("user.home"), "Library/LaunchAgents",
39+
backgroundLabel + ".plist").toAbsolutePath();
40+
private static final String windowsTaskName = "\\airsquared\\blobsaver\\BackgroundService";
3941

4042

4143
private static void macosBackgroundFile() {
42-
// String executablePath = Utils.getBlobsaverExecutable().getAbsolutePath();
4344
String executablePath = Utils.getBlobsaverExecutable().getAbsolutePath();
44-
long interval = Prefs.getBackgroundIntervalTimeUnit().toSeconds(Prefs.getBackgroundInterval());
45-
// eventually replace with Java 15 text blocks
45+
long interval = Prefs.getBackgroundTimeUnit().toSeconds(Prefs.getBackgroundInterval());
4646
//language=XML
4747
String plist = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
48-
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">" +
49-
"<plist>" +
50-
"<dict>" +
51-
"<key>Label</key>" +
52-
"<string>airsquared.blobsaver.app.BackgroundService</string>" +
53-
"<key>ProgramArguments</key>" +
54-
"<array>" +
55-
" <string>" + executablePath + "</string>" +
56-
" <string>--background-autosave</string>" +
57-
"</array>" +
58-
"<key>RunAtLoad</key>" +
59-
"<true/>" +
60-
"<key>StartInterval</key>" +
61-
"<integer>" + interval + "</integer>" +
62-
"</dict>" +
63-
"</plist>";
48+
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">" +
49+
"<plist>" +
50+
"<dict>" +
51+
"<key>Label</key>" +
52+
"<string>" + backgroundLabel + "</string>" +
53+
"<key>ProgramArguments</key>" +
54+
"<array>" +
55+
" <string>" + executablePath + "</string>" +
56+
" <string>--background-autosave</string>" +
57+
"</array>" +
58+
"<key>RunAtLoad</key>" +
59+
"<true/>" +
60+
"<key>StartInterval</key>" +
61+
"<integer>" + interval + "</integer>" +
62+
"</dict>" +
63+
"</plist>";
64+
try {
65+
Files.writeString(plistFilePath, plist);
66+
} catch (IOException e) {
67+
throw new UncheckedIOException(e);
68+
}
69+
}
70+
71+
private static void linuxBackgroundFile() {
72+
String executablePath = Utils.getBlobsaverExecutable().getAbsolutePath();
73+
String service = """
74+
[Unit]
75+
Description=Run blobsaver autosave
76+
77+
[Service]
78+
Type=oneshot
79+
ExecStart=%s --background-autosave""".formatted(executablePath);
80+
String timer = """
81+
[Unit]
82+
Description=Run blobsaver autosave
83+
84+
[Timer]
85+
OnBootSec=1min
86+
OnUnitInactiveSec=%dmin
87+
88+
[Install]
89+
WantedBy=timers.target
90+
""".formatted(Prefs.getBackgroundTimeUnit().toMinutes(Prefs.getBackgroundInterval()));
91+
String dataHome = System.getenv("XDG_DATA_HOME");
92+
if (dataHome == null) {
93+
dataHome = System.getProperty("user.home") + "/.local/share";
94+
}
95+
Path path = Path.of(dataHome + "/systemd/user");
6496
try {
65-
Files.write(Paths.get(plistFilePath), plist.getBytes(StandardCharsets.UTF_8));
97+
Files.writeString(path.resolve("blobsaver.service"), service);
98+
Files.writeString(path.resolve("blobsaver.timer"), timer);
6699
} catch (IOException e) {
67100
throw new UncheckedIOException(e);
68101
}
69102
}
70103

71104
public static void startBackground() {
72-
// TODO: throw exception and show to user if background is not available
73105
if (Platform.isMac()) {
74106
macosBackgroundFile();
75-
launchctl("load", "-w", plistFilePath);
107+
launchctl("load", "-w", plistFilePath.toString());
76108
} else if (Platform.isWindows()) {
77-
// TODO: schtasks
109+
// https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/schtasks-create
110+
schtasks("/create", "/it", "/f", "/tn", windowsTaskName,
111+
"/tr", Utils.getBlobsaverExecutable().getAbsolutePath() + " --background-autosave",
112+
"/sc", "once", "/st", new SimpleDateFormat("HH:mm").format(new Date()),
113+
"/ri", Long.toString(Prefs.getBackgroundTimeUnit().toMinutes(Prefs.getBackgroundInterval())));
78114
} else {
79-
// TODO: systemd timers
115+
linuxBackgroundFile();
116+
systemctl("daemon-reload");
117+
systemctl("enable", "--user", "--now", "blobsaver.timer");
118+
runOnce(); // systemd doesn't start it automatically when enabled
80119
}
81120
}
82121

83122
public static void stopBackground() {
84-
launchctl("unload", "-w", plistFilePath);
123+
if (Platform.isMac()) {
124+
launchctl("unload", "-w", plistFilePath.toString());
125+
} else if (Platform.isWindows()) {
126+
schtasks("/delete", "/f", "/tn", windowsTaskName);
127+
} else {
128+
systemctl("disable", "--user", "--now", "blobsaver.timer");
129+
}
85130
}
86131

87132
public static boolean isBackgroundEnabled() {
88-
// don't use Utils.executeProgram() because don't need to print any output
89-
try (BufferedReader reader = new BufferedReader(new InputStreamReader(
90-
new ProcessBuilder("/bin/launchctl", "list").start().getInputStream()))) {
91-
return reader.lines().anyMatch(s -> s.contains(backgroundLabel));
92-
} catch (IOException e) {
93-
throw new UncheckedIOException(e);
133+
if (Platform.isMac()) {
134+
return outputMatches(s -> s.contains(backgroundLabel), "/bin/launchctl", "list");
135+
} else if (Platform.isWindows()) {
136+
return outputMatches(s -> s.contains("Ready") || s.contains("Running"), "schtasks", "/Query", "/TN", windowsTaskName);
137+
} else {
138+
try {
139+
return new ProcessBuilder("systemctl", "is-enabled", "--user", "--quiet", "blobsaver.timer")
140+
.start().waitFor() == 0;
141+
} catch (IOException e) {
142+
throw new UncheckedIOException(e);
143+
} catch (InterruptedException e) {
144+
throw new RuntimeException(e);
145+
}
146+
}
147+
}
148+
149+
public static void runOnce() {
150+
if (Platform.isMac()) {
151+
launchctl("start", backgroundLabel);
152+
} else if (Platform.isWindows()) {
153+
schtasks("/run", windowsTaskName);
154+
} else {
155+
systemctl("start", "--user", "blobsaver.service");
94156
}
95157
}
96158

97159
private static void launchctl(String... args) {
160+
execute("launchctl", args);
161+
}
162+
163+
private static void schtasks(String... args) {
164+
execute("schtasks", args);
165+
}
166+
167+
private static void systemctl(String... args) {
168+
execute("systemctl", args);
169+
}
170+
171+
private static void execute(String program, String... args) {
98172
ArrayList<String> arguments = new ArrayList<>(args.length + 1);
99-
arguments.add("/bin/launchctl");
173+
arguments.add(program);
100174
Collections.addAll(arguments, args);
101175
try {
102176
Utils.executeProgram(arguments);
@@ -105,8 +179,13 @@ private static void launchctl(String... args) {
105179
}
106180
}
107181

108-
public static void runOnce() {
109-
launchctl("start", backgroundLabel);
182+
private static boolean outputMatches(Predicate<String> predicate, String... args) {
183+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(
184+
new ProcessBuilder(args).redirectErrorStream(true).start().getInputStream()))) {
185+
return reader.lines().anyMatch(predicate);
186+
} catch (IOException e) {
187+
throw new UncheckedIOException(e);
188+
}
110189
}
111190

112191
public static void saveAllBackgroundBlobs() {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ public void chooseTimeToRunHandler() {
404404
textField.setTextFormatter(Utils.intOnlyFormatter());
405405

406406
ChoiceBox<String> choiceBox = new ChoiceBox<>(FXCollections.observableArrayList("Minutes", "Hours", "Days", "Weeks"));
407-
choiceBox.setValue(Prefs.getBackgroundIntervalTimeUnit().toString());
407+
choiceBox.setValue(Prefs.getBackgroundTimeUnit().toString());
408408

409409
alert.getDialogPane().setContent(new HBox(textField, choiceBox));
410410
alert.showAndWait();

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public class Main {
4343
static {
4444
try {
4545
jarDirectory = new File(System.getProperty("jar.directory")).getCanonicalFile();
46+
System.out.println(jarDirectory);
4647
} catch (IOException e) {
4748
throw new UncheckedIOException(e);
4849
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ public static long getBackgroundInterval() {
9797
return appPrefs.getLong("Time to run", 1);
9898
}
9999

100-
public static TimeUnit getBackgroundIntervalTimeUnit() {
100+
public static TimeUnit getBackgroundTimeUnit() {
101101
return TimeUnit.valueOf(appPrefs.get("Time unit for background", "DAYS"));
102102
}
103103

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ static void checkForUpdates(boolean forceCheck) {
107107
ButtonType downloadNow = new ButtonType("Download");
108108
ButtonType ignore = new ButtonType("Ignore this update");
109109
Alert alert = new Alert(Alert.AlertType.INFORMATION, "You have version "
110-
+ Main.appVersion + "\n\n" + changelog, downloadNow, ignore, ButtonType.CANCEL);
110+
+ Main.appVersion + "\n\n" + changelog, downloadNow, ignore, ButtonType.CANCEL);
111111
alert.setHeaderText("New Update Available: " + newVersion);
112112
alert.setTitle("New Update Available for blobsaver");
113113
Button dlButton = (Button) alert.getDialogPane().lookupButton(downloadNow);
@@ -162,7 +162,14 @@ static File getTsschecker() {
162162
static File getBlobsaverExecutable() {
163163
if (blobsaverExecutable != null) return blobsaverExecutable;
164164

165-
blobsaverExecutable = new File(Main.jarDirectory, "MacOS/blobsaver");
165+
if (Platform.isMac()) {
166+
blobsaverExecutable = new File(Main.jarDirectory, "MacOS/blobsaver");
167+
} else if (Platform.isWindows()) {
168+
blobsaverExecutable = new File(Main.jarDirectory, "blobsaver.exe");
169+
} else {
170+
blobsaverExecutable = new File(Main.jarDirectory.getParentFile(), "bin/blobsaver");
171+
}
172+
166173
return blobsaverExecutable;
167174
}
168175

0 commit comments

Comments
 (0)