1   package org.daisy.pipeline.tts;
2   
3   import java.util.concurrent.atomic.AtomicBoolean;
4   
5   /**
6    * TTSTimeout keeps watch over a given thread (which can be the current thread).
7    * When the time is up, it first attempts to call interrupt() on the thread. If
8    * this is not working, it will try by calling a custom function (e.g. something
9    * that brutally kills a process).
10   * 
11   * As long as it is used sequentially, TTSTimeout can be called again multiple
12   * times : no extra thread creation will be performed.
13   * 
14   */
15  public class TTSTimeout extends Thread {
16  
17  	public interface ThreadFreeInterrupter {
18  		void threadFreeInterrupt();
19  	}
20  
21  	private Thread mJob;
22  	private int mSeconds;
23  	private AtomicBoolean mClosed;
24  	private boolean mEnable;
25  	private ThreadFreeInterrupter mInterrupter;
26  
27  	public TTSTimeout() {
28  		mClosed = new AtomicBoolean(false);
29  		mEnable = false;
30  		mInterrupter = null;
31  		start();
32  	}
33  
34  	/**
35  	 * Must always be called even if enable() is never called, otherwise the
36  	 * underlying thread will run forever.
37  	 */
38  	public void close() {
39  		mClosed.set(true);
40  		this.interrupt(); //interrupt the wait at the beginning of the loop
41  	}
42  
43  	public void enable(Thread job, ThreadFreeInterrupter interruptable, int seconds) {
44  		if (seconds < 0)
45  			throw new IllegalArgumentException("Expecting a positive number of seconds");
46  		mInterrupter = interruptable;
47  		mJob = job;
48  		mEnable = true;
49  		mSeconds = seconds;
50  		synchronized (mClosed) {
51  			mClosed.notify(); //unblock the wait at the beginning of the loop
52  		}
53  	}
54  
55  	public void enable(Thread job, int seconds) {
56  		enable(job, null, seconds);
57  	}
58  
59  	public void enableForCurrentThread(ThreadFreeInterrupter interruptable, int seconds) {
60  		enable(Thread.currentThread(), interruptable, seconds);
61  	}
62  
63  	public void enableForCurrentThread(int seconds) {
64  		enableForCurrentThread(null, seconds);
65  	}
66  
67  	/**
68  	 * Must always be called after enable().
69  	 */
70  	public void disable() {
71  		this.interrupt();
72  
73  		//wait for the timeout to finish its current loop
74  		synchronized (mClosed) {
75  			while (mEnable) {
76  				try {
77  					mClosed.wait();
78  				} catch (InterruptedException e) {
79  					//not possible: two disable()'s or close() cannot be called simultaneously
80  				}
81  			}
82  		}
83  	}
84  
85  	@Override
86  	public void run() {
87  		while (!mClosed.get()) {
88  			synchronized (mClosed) {
89  				while (!mEnable) {
90  					try {
91  						mClosed.wait();
92  					} catch (InterruptedException e) {
93  						//happen when close() is called
94  						return;
95  					}
96  				}
97  			}
98  			try {
99  				sleep(Math.multiplyExact(mSeconds, 1000));
100 			} catch (InterruptedException e) {
101 				//interrupted by the TTS thread with disable() because the watched job is finished
102 				notifyFinished();
103 				continue;
104 			}
105 			//time's up! first attempt: the job is gracefully interrupted
106 			mJob.interrupt();
107 
108 			//wait for the job to exit and to cancel this timeout
109 			try {
110 				sleep(1000);
111 			} catch (InterruptedException e) {
112 				//interrupted by the TTS thread with disable()
113 				notifyFinished();
114 				continue;
115 			}
116 			if (interrupted()) {
117 				//can happen if the thread is interrupted between calls
118 				notifyFinished();
119 				continue;
120 			}
121 
122 			//it didn't work: we attempt the aggressive way
123 			if (mInterrupter != null)
124 				mInterrupter.threadFreeInterrupt();
125 
126 			//wait forever until the TTS cancels this timeout
127 			try {
128 				sleep(Long.MAX_VALUE);
129 			} catch (InterruptedException e) {
130 				//interrupted by the TTS thread with disable()
131 				notifyFinished();
132 				continue;
133 			}
134 		}
135 	}
136 
137 	private void notifyFinished() {
138 		mEnable = false;
139 		synchronized (mClosed) {
140 			mClosed.notify();
141 		}
142 	}
143 
144 }