2424import java .io .IOException ;
2525import java .io .InputStreamReader ;
2626import java .io .UncheckedIOException ;
27- import java .nio .charset .StandardCharsets ;
2827import java .nio .file .Files ;
29- import java .nio .file .Paths ;
28+ import java .nio .file .Path ;
29+ import java .text .SimpleDateFormat ;
3030import java .util .ArrayList ;
3131import java .util .Collections ;
32+ import java .util .Date ;
33+ import java .util .function .Predicate ;
3234
3335class 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 () {
0 commit comments