Last active
September 27, 2017 13:39
-
-
Save basinilya/c54fe130fd21f642441218e6812df4b7 to your computer and use it in GitHub Desktop.
PidFileLock
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
I want to implement a portable pidfile management using nio FileLock. Apparently, there are no other ways in java to lock a file (flock(2) or ReOpenFile() with a different dwShareMode). It should allow other programs read my pidfile, particularly on Windows. On Linux a second instance of my program should be unable to lock the pidfile. | |
The problem is that at least on Windows an exclusive lock prevents other programs from reading it and a shared lock prevents myself from writing to the channel I just locked: | |
final FileLock lock = ch.lock(0L, Long.MAX_VALUE, true); | |
ch.write(ByteBuffer.wrap(new byte[] { 0 })); | |
_ | |
The process cannot access the file because another process has locked a portion of the file | |
To allow other programs read the pidfile on windows most of the time it has to be a shared lock, but when my program starts it has to create an exclusive lock to be sure that there's no other instance of my program. | |
I cannot resize or convert an existing FileLock. I cannot create a shared lock without removing an existing exclusive lock on the same region. And if I remove it, during this short time another instance will be able to put its own lock. | |
This class does the following: | |
Put two exclusive locks - to first and last bytes of the file. | |
write my pid into file | |
release the exclusive lock from the last byte | |
put a shared lock to the last byte | |
release the lock from the first byte |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.foo.pidfilelock; | |
import java.io.Closeable; | |
import java.io.File; | |
import java.io.IOException; | |
import java.io.RandomAccessFile; | |
import java.io.Reader; | |
import java.lang.management.ManagementFactory; | |
import java.nio.CharBuffer; | |
import java.nio.channels.Channels; | |
import java.nio.channels.FileChannel; | |
import java.nio.channels.FileLock; | |
import java.nio.charset.Charset; | |
import java.nio.file.Files; | |
/** | |
* Create a pidfile or fails if the pidfile already locked by another program. | |
*/ | |
public class PidFileLock implements Closeable { | |
private final FileLock sharedLock; | |
private final FileChannel keepSecondCh; | |
private final File pidfile; | |
private final String oldPid; | |
public String getOldPid() { | |
return oldPid; | |
} | |
public PidFileLock(final String sPidfile) throws IOException { | |
pidfile = new File(sPidfile); | |
final FileChannel ch = open("rw"); | |
boolean ok = false; | |
String ownerPid = null; | |
try { | |
final FileLock lastByteLock = ch.tryLock(Integer.MAX_VALUE - 1, 1, false); | |
final FileLock firstBytesLock = ch.tryLock(0, Integer.MAX_VALUE - 1, false); | |
ownerPid = readAll(ch, ENCODING); | |
if (lastByteLock != null && firstBytesLock != null) { | |
ch.truncate(0); | |
final String sPid = getPid(); | |
ch.write(Charset.forName(ENCODING).newEncoder().encode(CharBuffer.wrap(sPid))); | |
firstBytesLock.release(); | |
sharedLock = ch.tryLock(0, Integer.MAX_VALUE - 1, true); | |
if (sharedLock == null) { | |
throw new IOException("Something wrong"); | |
} | |
lastByteLock.release(); | |
/* | |
* if we opened an existing pidfile and it was deleted and closed by the previous | |
* process immediately after that, then we can still read and lock the file although | |
* it's deleted. Meanwhile a third process can create its own pidfile. | |
*/ | |
oldPid = ownerPid; | |
keepSecondCh = open("r"); | |
try { | |
ownerPid = readAll(keepSecondCh, ENCODING); | |
/* | |
* Not closing the second fd here. See FileLock javadoc: On some systems, | |
* closing a channel releases all locks held by the Java virtual machine on the | |
* underlying file regardless of whether the locks were acquired via that | |
* channel or via another channel open on the same file. | |
*/ | |
ok = ownerPid.equals(sPid); | |
} finally { | |
if (ok) { | |
return; | |
} | |
keepSecondCh.close(); | |
} | |
} | |
throw new IOException("already locked by: " + ownerPid); | |
} finally { | |
if (!ok) { | |
ch.close(); | |
} | |
} | |
} | |
public static String getPid() { | |
String sPid = ManagementFactory.getRuntimeMXBean().getName(); | |
sPid = sPid.substring(0, sPid.indexOf('@')); | |
return sPid; | |
} | |
@Override | |
public void close() throws IOException { | |
boolean alreadyDeleted = true; | |
FileChannel thirdCh = null; | |
try { | |
thirdCh = open("r"); | |
final String ownerPid = readAll(thirdCh, ENCODING); | |
/* Until delete attempt not closing the third fd for the same reason as the second fd */ | |
// Do I still own the pidfile? | |
alreadyDeleted = !ownerPid.equals(getPid()); | |
if (alreadyDeleted) { | |
throw new IOException("not deleting '" + pidfile + "', because process " + ownerPid | |
+ " owns it"); | |
} else { | |
// If this fails, will try a throwing delete after close | |
alreadyDeleted = pidfile.delete(); | |
} | |
} finally { | |
if (thirdCh != null) { | |
thirdCh.close(); | |
} | |
keepSecondCh.close(); | |
sharedLock.channel().close(); | |
if (!alreadyDeleted) { | |
Files.delete(pidfile.toPath()); | |
} | |
} | |
} | |
@SuppressWarnings("resource") | |
private FileChannel open(final String mode) throws IOException { | |
final RandomAccessFile f = new RandomAccessFile(pidfile, mode); | |
final FileChannel ch = f.getChannel(); | |
return ch; | |
} | |
private static String readAll(final FileChannel ch, final String csName) throws IOException { | |
// TODO: use final String sPid = IOUtils.toString(Channels.newReader(ch, ENCODING)); | |
final StringBuilder sb = new StringBuilder((int) ch.size()); | |
final Reader r = Channels.newReader(ch, csName); | |
final char[] cbuf = new char[100]; | |
int len; | |
while ((len = r.read(cbuf)) != -1) { | |
sb.append(cbuf, 0, len); | |
} | |
return sb.toString(); | |
} | |
private static final String ENCODING = "UTF-8"; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment