001 package com.github.sarxos.webcam;
002
003 import java.io.DataInputStream;
004 import java.io.DataOutputStream;
005 import java.io.EOFException;
006 import java.io.File;
007 import java.io.FileInputStream;
008 import java.io.FileOutputStream;
009 import java.io.IOException;
010 import java.util.concurrent.atomic.AtomicBoolean;
011
012 import org.slf4j.Logger;
013 import 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 */
026 public 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 }