Skip to content

Instantly share code, notes, and snippets.

@basinilya
Last active September 27, 2017 13:39
Show Gist options
  • Save basinilya/c54fe130fd21f642441218e6812df4b7 to your computer and use it in GitHub Desktop.
Save basinilya/c54fe130fd21f642441218e6812df4b7 to your computer and use it in GitHub Desktop.
PidFileLock
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
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