package com.github.sarxos.webcam; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.EOFException; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.concurrent.atomic.AtomicBoolean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class is used as a global (system) lock preventing other processes from using the same * camera while it's open. Whenever webcam is open there is a thread running in background which * updates the lock once per 2 seconds. Lock is being released whenever webcam is either closed or * completely disposed. Lock will remain for at least 2 seconds in case when JVM has not been * gracefully terminated (due to SIGSEGV, SIGTERM, etc). * * @author Bartosz Firyn (sarxos) */ public class WebcamLock { /** * Logger. */ private static final Logger LOG = LoggerFactory.getLogger(WebcamLock.class); /** * Update interval (ms). */ public static final long INTERVAL = 2000; /** * Used to update lock state. * * @author sarxos */ private class LockUpdater extends Thread { public LockUpdater() { super(); setName(String.format("webcam-lock-[%s]", webcam.getName())); setDaemon(true); setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance()); } @Override public void run() { do { if (disabled.get()) { return; } update(); try { Thread.sleep(INTERVAL); } catch (InterruptedException e) { LOG.debug("Lock updater has been interrupted"); return; } } while (locked.get()); } } /** * And the Webcam we will be locking. */ private final Webcam webcam; /** * Updater thread. It will update the lock value in fixed interval. */ private Thread updater = null; /** * Is webcam locked (local, not cross-VM variable). */ private final AtomicBoolean locked = new AtomicBoolean(false); /** * Is lock completely disabled. */ private final AtomicBoolean disabled = new AtomicBoolean(false); /** * Lock file. */ private final File lock; /** * Creates global webcam lock. * * @param webcam the webcam instance to be locked */ protected WebcamLock(Webcam webcam) { super(); this.webcam = webcam; this.lock = new File(System.getProperty("java.io.tmpdir"), getLockName()); this.lock.deleteOnExit(); } private String getLockName() { return String.format(".webcam-lock-%d", Math.abs(webcam.getName().hashCode())); } private void write(long value) { if (disabled.get()) { return; } String name = getLockName(); File tmp = null; DataOutputStream dos = null; try { tmp = File.createTempFile(String.format("%s-tmp", name), ""); tmp.deleteOnExit(); dos = new DataOutputStream(new FileOutputStream(tmp)); dos.writeLong(value); dos.flush(); } catch (IOException e) { throw new RuntimeException(e); } finally { if (dos != null) { try { dos.close(); } catch (IOException e) { throw new RuntimeException(e); } } } if (!locked.get()) { return; } if (tmp.renameTo(lock)) { // atomic rename operation can fail (mostly on Windows), so we // simply jump out the method if it succeed, or try to rewrite // content using streams if it fail return; } else { // create lock file if not exist if (!lock.exists()) { try { if (lock.createNewFile()) { LOG.info("Lock file {} for {} has been created", lock, webcam); } else { throw new RuntimeException("Not able to create file " + lock); } } catch (IOException e) { throw new RuntimeException(e); } } FileOutputStream fos = null; FileInputStream fis = null; int k = 0; int n = -1; byte[] buffer = new byte[8]; boolean rewritten = false; // rewrite temporary file content to lock, try max 5 times synchronized (webcam) { do { try { fos = new FileOutputStream(lock); fis = new FileInputStream(tmp); while ((n = fis.read(buffer)) != -1) { fos.write(buffer, 0, n); } rewritten = true; } catch (IOException e) { LOG.debug("Not able to rewrite lock file", e); } finally { if (fos != null) { try { fos.close(); } catch (IOException e) { throw new RuntimeException(e); } } if (fis != null) { try { fis.close(); } catch (IOException e) { throw new RuntimeException(e); } } } if (rewritten) { break; } } while (k++ < 5); } if (!rewritten) { throw new WebcamException("Not able to write lock file"); } // remove temporary file if (!tmp.delete()) { tmp.deleteOnExit(); } } } private long read() { if (disabled.get()) { return -1; } DataInputStream dis = null; long value = -1; boolean broken = false; synchronized (webcam) { try { value = (dis = new DataInputStream(new FileInputStream(lock))).readLong(); } catch (EOFException e) { LOG.debug("Webcam lock is broken - EOF when reading long variable from stream", e); broken = true; } catch (IOException e) { throw new RuntimeException(e); } finally { if (dis != null) { try { dis.close(); } catch (IOException e) { throw new RuntimeException(e); } } } if (broken) { LOG.warn("Lock file {} for {} is broken - recreating it", lock, webcam); write(-1); } } return value; } private void update() { if (disabled.get()) { return; } write(System.currentTimeMillis()); } /** * Lock webcam. */ public void lock() { if (disabled.get()) { return; } if (isLocked()) { throw new WebcamLockException(String.format("Webcam %s has already been locked", webcam.getName())); } if (!locked.compareAndSet(false, true)) { return; } LOG.debug("Lock {}", webcam); update(); updater = new LockUpdater(); updater.start(); } /** * Completely disable locking mechanism. After this method is invoked, the lock will not have * any effect on the webcam runtime. */ public void disable() { if (disabled.compareAndSet(false, true)) { LOG.info("Locking mechanism has been disabled in {}", webcam); if (updater != null) { updater.interrupt(); } } } /** * Unlock webcam. */ public void unlock() { // do nothing when lock disabled if (disabled.get()) { return; } if (!locked.compareAndSet(true, false)) { return; } LOG.debug("Unlock {}", webcam); updater.interrupt(); write(-1); if (!lock.delete()) { lock.deleteOnExit(); } } /** * Check if webcam is locked. * * @return True if webcam is locked, false otherwise */ public boolean isLocked() { // always return false when lock is disabled if (disabled.get()) { return false; } // check if locked by current process if (locked.get()) { return true; } // check if locked by other process if (!lock.exists()) { return false; } long now = System.currentTimeMillis(); long tsp = read(); LOG.trace("Lock timestamp {} now {} for {}", tsp, now, webcam); if (tsp > now - INTERVAL * 2) { return true; } return false; } public File getLockFile() { return lock; } }