1
1
package mill .scalalib .backgroundwrapper ;
2
2
3
+ import java .io .IOException ;
3
4
import java .io .RandomAccessFile ;
4
5
import java .nio .channels .FileChannel ;
5
6
import java .nio .file .*;
7
+ import java .util .Optional ;
6
8
9
+ @ SuppressWarnings ("BusyWait" )
7
10
public class MillBackgroundWrapper {
8
11
public static void main (String [] args ) throws Exception {
9
- Path procUuidPath = Paths .get (args [0 ]);
10
- Path procLockfile = Paths .get (args [1 ]);
11
- String procUuid = args [2 ];
12
- int lockDelay = Integer .parseInt (args [3 ]);
12
+ var newestProcessIdPath = Paths .get (args [0 ]);
13
+ var currentlyRunningProccesIdPath = Paths .get (args [1 ]);
14
+ var procLockfile = Paths .get (args [2 ]);
15
+ var realMain = args [3 ];
16
+ var realArgs = java .util .Arrays .copyOfRange (args , 4 , args .length );
13
17
14
- Files .writeString (procUuidPath , procUuid , StandardOpenOption .CREATE );
18
+ // The following code should handle this scenario, when we have 2 processes contending for the
19
+ // lock.
20
+ //
21
+ // This can happen if a new MillBackgroundWrapper is launched while the previous one is still
22
+ // running due to rapid
23
+ // changes in the source code.
24
+ //
25
+ // Process 1 starts, writes newest_pid=1, claims lock, writes currently_running_pid = 1
26
+ // Process 2 starts, writes newest_pid=2, tries to claim lock but is blocked
27
+ // Process 3 starts at the same time as process 2, writes newest_pid=3, tries to claim lock but
28
+ // is blocked
29
+ //
30
+ // Process 1 reads newest_pid=3, terminates, releases lock
31
+ // Process 2 claims lock, reads currently_running_pid = 1, waits for process 1 to die, writes
32
+ // currently_running_pid = 2
33
+ // Process 2 reads newest_pid=3, terminates, releases lock
34
+ // Process 3 claims lock, reads currently_running_pid = 2, waits for process 2 to die, writes
35
+ // currently_running_pid = 3, then starts
36
+ // Process 3 reads newest_pid=3, continues running
37
+
38
+ // Indicate to the previous process that we want to take over.
39
+ var myPid = ProcessHandle .current ().pid ();
40
+ var myPidStr = "" + myPid ;
41
+ Files .writeString (newestProcessIdPath , myPidStr , StandardOpenOption .CREATE );
15
42
16
43
// Take a lock on `procLockfile` to ensure that only one
17
44
// `runBackground` process is running at any point in time.
45
+ //noinspection resource - this is intentional, file is released when process dies.
18
46
RandomAccessFile raf = new RandomAccessFile (procLockfile .toFile (), "rw" );
19
47
FileChannel chan = raf .getChannel ();
20
48
if (chan .tryLock () == null ) {
21
49
System .err .println ("Waiting for runBackground lock to be available" );
50
+ //noinspection ResultOfMethodCallIgnored - this is intentional, lock is released when process dies.
22
51
chan .lock ();
23
52
}
24
53
25
- // For some reason even after the previous process exits things like sockets
26
- // may still take time to free, so sleep for a configurable duration before proceeding
27
- Thread . sleep ( lockDelay );
54
+ var oldProcessPid = readPreviousPid ( currentlyRunningProccesIdPath );
55
+ oldProcessPid . ifPresent ( MillBackgroundWrapper :: waitForPreviousProcessToTerminate );
56
+ Files . writeString ( currentlyRunningProccesIdPath , myPidStr , StandardOpenOption . CREATE );
28
57
29
58
// Start the thread to watch for updates on the process marker file,
30
59
// so we can exit if it is deleted or replaced
31
- long startTime = System .currentTimeMillis ();
32
- Thread watcher = new Thread (() -> {
33
- while (true ) {
34
- long delta = (System .currentTimeMillis () - startTime ) / 1000 ;
35
- try {
36
- Thread .sleep (1 );
37
- String token = Files .readString (procUuidPath );
38
- if (!token .equals (procUuid )) {
39
- System .err .println ("runBackground exiting after " + delta + "s" );
40
- System .exit (0 );
41
- }
42
- } catch (Exception e ) {
43
- System .err .println ("runBackground exiting after " + delta + "s" );
44
- System .exit (0 );
45
- }
46
- }
60
+ var startTime = System .currentTimeMillis ();
61
+ checkIfWeStillNeedToBeRunning (startTime , newestProcessIdPath , myPidStr );
62
+ var watcher = new Thread (() -> {
63
+ while (true ) checkIfWeStillNeedToBeRunning (startTime , newestProcessIdPath , myPidStr );
47
64
});
48
-
49
65
watcher .setDaemon (true );
50
66
watcher .start ();
51
67
52
68
// Actually start the Java main method we wanted to run in the background
53
- String realMain = args [4 ];
54
- String [] realArgs = java .util .Arrays .copyOfRange (args , 5 , args .length );
55
69
if (!realMain .equals ("<subprocess>" )) {
56
70
Class .forName (realMain ).getMethod ("main" , String [].class ).invoke (null , (Object ) realArgs );
57
71
} else {
@@ -62,17 +76,58 @@ public static void main(String[] args) throws Exception {
62
76
63
77
long now = System .currentTimeMillis ();
64
78
79
+ // If the process does not shut down withing 100ms kill it forcibly.
65
80
while (subprocess .isAlive () && System .currentTimeMillis () - now < 100 ) {
66
81
try {
67
82
Thread .sleep (1 );
68
83
} catch (InterruptedException e ) {
84
+ // do nothing
69
85
}
70
- if ( subprocess . isAlive ()) {
71
- subprocess .destroyForcibly ();
72
- }
86
+ }
87
+ if ( subprocess .isAlive ()) {
88
+ subprocess . destroyForcibly ();
73
89
}
74
90
}));
75
91
System .exit (subprocess .waitFor ());
76
92
}
77
93
}
94
+
95
+ private static void checkIfWeStillNeedToBeRunning (
96
+ long startTime , Path newestProcessIdPath , String myPidStr ) {
97
+ long delta = (System .currentTimeMillis () - startTime ) / 1000 ;
98
+ try {
99
+ Thread .sleep (50 );
100
+ String token = Files .readString (newestProcessIdPath );
101
+ if (!token .equals (myPidStr )) {
102
+ System .err .println ("runBackground exiting after " + delta + "s" );
103
+ System .exit (0 );
104
+ }
105
+ } catch (Exception e ) {
106
+ System .err .println ("runBackground exiting after " + delta + "s" );
107
+ System .exit (0 );
108
+ }
109
+ }
110
+
111
+ static Optional <Long > readPreviousPid (Path pidFilePath ) {
112
+ try {
113
+ var pidStr = Files .readString (pidFilePath );
114
+ return Optional .of (Long .parseLong (pidStr ));
115
+ } catch (IOException | NumberFormatException e ) {
116
+ return Optional .empty ();
117
+ }
118
+ }
119
+
120
+ static void waitForPreviousProcessToTerminate (long pid ) {
121
+ var maybeOldProcess = ProcessHandle .of (pid );
122
+ if (maybeOldProcess .isEmpty ()) return ;
123
+ var oldProcess = maybeOldProcess .get ();
124
+
125
+ try {
126
+ while (oldProcess .isAlive ()) {
127
+ Thread .sleep (50 );
128
+ }
129
+ } catch (InterruptedException e ) {
130
+ throw new RuntimeException (e );
131
+ }
132
+ }
78
133
}
0 commit comments