001package com.github.sarxos.webcam; 002 003import java.io.DataInputStream; 004import java.io.DataOutputStream; 005import java.io.EOFException; 006import java.io.File; 007import java.io.FileInputStream; 008import java.io.FileOutputStream; 009import java.io.IOException; 010import java.util.concurrent.atomic.AtomicBoolean; 011 012import org.slf4j.Logger; 013import org.slf4j.LoggerFactory; 014 015 016/** 017 * This class is used as a global (system) lock preventing other processes from 018 * using the same camera while it's open. Whenever webcam is open there is a 019 * thread running in background which updates the lock once per 2 seconds. Lock 020 * is being released whenever webcam is either closed or completely disposed. 021 * Lock will remain for at least 2 seconds in case when JVM has not been 022 * gracefully terminated (due to SIGSEGV, SIGTERM, etc). 023 * 024 * @author Bartosz Firyn (sarxos) 025 */ 026public class WebcamLock { 027 028 /** 029 * Logger. 030 */ 031 private static final Logger LOG = LoggerFactory.getLogger(WebcamLock.class); 032 033 /** 034 * Update interval (ms). 035 */ 036 public static final long INTERVAL = 2000; 037 038 /** 039 * Used to update lock state. 040 * 041 * @author sarxos 042 */ 043 private class LockUpdater extends Thread { 044 045 public LockUpdater() { 046 super(); 047 setName(String.format("webcam-lock-[%s]", webcam.getName())); 048 setDaemon(true); 049 setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance()); 050 } 051 052 @Override 053 public void run() { 054 do { 055 if (disabled.get()) { 056 return; 057 } 058 update(); 059 try { 060 Thread.sleep(INTERVAL); 061 } catch (InterruptedException e) { 062 LOG.debug("Lock updater has been interrupted"); 063 return; 064 } 065 } while (locked.get()); 066 } 067 068 } 069 070 /** 071 * And the Webcam we will be locking. 072 */ 073 private final Webcam webcam; 074 075 /** 076 * Updater thread. It will update the lock value in fixed interval. 077 */ 078 private Thread updater = null; 079 080 /** 081 * Is webcam locked (local, not cross-VM variable). 082 */ 083 private AtomicBoolean locked = new AtomicBoolean(false); 084 085 /** 086 * Is lock completely disabled. 087 */ 088 private AtomicBoolean disabled = new AtomicBoolean(false); 089 090 /** 091 * Lock file. 092 */ 093 private File lock = null; 094 095 /** 096 * Creates global webcam lock. 097 * 098 * @param webcam the webcam instance to be locked 099 */ 100 protected WebcamLock(Webcam webcam) { 101 super(); 102 this.webcam = webcam; 103 this.lock = new File(System.getProperty("java.io.tmpdir"), getLockName()); 104 this.lock.deleteOnExit(); 105 } 106 107 private String getLockName() { 108 return String.format(".webcam-lock-%d", Math.abs(webcam.getName().hashCode())); 109 } 110 111 private void write(long value) { 112 113 if (disabled.get()) { 114 return; 115 } 116 117 String name = getLockName(); 118 119 File tmp = null; 120 DataOutputStream dos = null; 121 122 try { 123 124 tmp = File.createTempFile(String.format("%s-tmp", name), ""); 125 tmp.deleteOnExit(); 126 127 dos = new DataOutputStream(new FileOutputStream(tmp)); 128 dos.writeLong(value); 129 dos.flush(); 130 131 } catch (IOException e) { 132 throw new RuntimeException(e); 133 } finally { 134 if (dos != null) { 135 try { 136 dos.close(); 137 } catch (IOException e) { 138 throw new RuntimeException(e); 139 } 140 } 141 } 142 143 if (!locked.get()) { 144 return; 145 } 146 147 if (tmp.renameTo(lock)) { 148 149 // atomic rename operation can fail (mostly on Windows), so we 150 // simply jump out the method if it succeed, or try to rewrite 151 // content using streams if it fail 152 153 return; 154 } else { 155 156 // create lock file if not exist 157 158 if (!lock.exists()) { 159 try { 160 if (lock.createNewFile()) { 161 LOG.info("Lock file {} for {} has been created", lock, webcam); 162 } else { 163 throw new RuntimeException("Not able to create file " + lock); 164 } 165 } catch (IOException e) { 166 throw new RuntimeException(e); 167 } 168 } 169 170 FileOutputStream fos = null; 171 FileInputStream fis = null; 172 173 int k = 0; 174 int n = -1; 175 byte[] buffer = new byte[8]; 176 boolean rewritten = false; 177 178 // rewrite temporary file content to lock, try max 5 times 179 180 synchronized (webcam) { 181 do { 182 try { 183 184 fos = new FileOutputStream(lock); 185 fis = new FileInputStream(tmp); 186 while ((n = fis.read(buffer)) != -1) { 187 fos.write(buffer, 0, n); 188 } 189 rewritten = true; 190 191 } catch (IOException e) { 192 LOG.debug("Not able to rewrite lock file", e); 193 } finally { 194 if (fos != null) { 195 try { 196 fos.close(); 197 } catch (IOException e) { 198 throw new RuntimeException(e); 199 } 200 } 201 if (fis != null) { 202 try { 203 fis.close(); 204 } catch (IOException e) { 205 throw new RuntimeException(e); 206 } 207 } 208 } 209 if (rewritten) { 210 break; 211 } 212 } while (k++ < 5); 213 } 214 215 if (!rewritten) { 216 throw new WebcamException("Not able to write lock file"); 217 } 218 219 // remove temporary file 220 221 if (!tmp.delete()) { 222 tmp.deleteOnExit(); 223 } 224 } 225 226 } 227 228 private long read() { 229 230 if (disabled.get()) { 231 return -1; 232 } 233 234 DataInputStream dis = null; 235 236 long value = -1; 237 boolean broken = false; 238 239 synchronized (webcam) { 240 241 try { 242 value = (dis = new DataInputStream(new FileInputStream(lock))).readLong(); 243 } catch (EOFException e) { 244 LOG.debug("Webcam lock is broken - EOF when reading long variable from stream", e); 245 broken = true; 246 } catch (IOException e) { 247 throw new RuntimeException(e); 248 } finally { 249 if (dis != null) { 250 try { 251 dis.close(); 252 } catch (IOException e) { 253 throw new RuntimeException(e); 254 } 255 } 256 } 257 258 if (broken) { 259 LOG.warn("Lock file {} for {} is broken - recreating it", lock, webcam); 260 write(-1); 261 } 262 } 263 264 return value; 265 } 266 267 private void update() { 268 269 if (disabled.get()) { 270 return; 271 } 272 273 write(System.currentTimeMillis()); 274 } 275 276 /** 277 * Lock webcam. 278 */ 279 public void lock() { 280 281 if (disabled.get()) { 282 return; 283 } 284 285 if (isLocked()) { 286 throw new WebcamLockException(String.format("Webcam %s has already been locked", webcam.getName())); 287 } 288 289 if (!locked.compareAndSet(false, true)) { 290 return; 291 } 292 293 LOG.debug("Lock {}", webcam); 294 295 update(); 296 297 updater = new LockUpdater(); 298 updater.start(); 299 } 300 301 /** 302 * Completely disable locking mechanism. After this method is invoked, the 303 * lock will not have any effect on the webcam runtime. 304 */ 305 public void disable() { 306 if (disabled.compareAndSet(false, true)) { 307 LOG.info("Locking mechanism has been disabled in {}", webcam); 308 if (updater != null) { 309 updater.interrupt(); 310 } 311 } 312 } 313 314 /** 315 * Unlock webcam. 316 */ 317 public void unlock() { 318 319 // do nothing when lock disabled 320 321 if (disabled.get()) { 322 return; 323 } 324 325 if (!locked.compareAndSet(true, false)) { 326 return; 327 } 328 329 LOG.debug("Unlock {}", webcam); 330 331 updater.interrupt(); 332 333 write(-1); 334 335 if (!lock.delete()) { 336 lock.deleteOnExit(); 337 } 338 } 339 340 /** 341 * Check if webcam is locked. 342 * 343 * @return True if webcam is locked, false otherwise 344 */ 345 public boolean isLocked() { 346 347 // always return false when lock is disabled 348 349 if (disabled.get()) { 350 return false; 351 } 352 353 // check if locked by current process 354 355 if (locked.get()) { 356 return true; 357 } 358 359 // check if locked by other process 360 361 if (!lock.exists()) { 362 return false; 363 } 364 365 long now = System.currentTimeMillis(); 366 long tsp = read(); 367 368 LOG.trace("Lock timestamp {} now {} for {}", tsp, now, webcam); 369 370 if (tsp > now - INTERVAL * 2) { 371 return true; 372 } 373 374 return false; 375 } 376}