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}