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    }