View Javadoc
1   /*
2    * Copyright (c) 2002-2025 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.javascript.background;
16  
17  import java.lang.ref.WeakReference;
18  import java.util.ArrayList;
19  import java.util.List;
20  import java.util.concurrent.atomic.AtomicBoolean;
21  
22  import org.apache.commons.logging.Log;
23  import org.apache.commons.logging.LogFactory;
24  import org.htmlunit.WebClient;
25  import org.htmlunit.WebWindow;
26  
27  /**
28   * An event loop to execute all the JavaScript jobs.
29   *
30   * @author Amit Manjhi
31   * @author Kostadin Chikov
32   * @author Ronald Brill
33   */
34  public class DefaultJavaScriptExecutor implements JavaScriptExecutor {
35  
36      private final transient WeakReference<WebClient> webClient_;
37      private final transient List<WeakReference<JavaScriptJobManager>> jobManagerList_;
38  
39      private final transient AtomicBoolean shutdown_;
40  
41      private transient Thread eventLoopThread_;
42  
43      /** Logging support. */
44      private static final Log LOG = LogFactory.getLog(DefaultJavaScriptExecutor.class);
45  
46      /**
47       * Creates an EventLoop for the webClient.
48       *
49       * @param webClient the provided webClient
50       */
51      public DefaultJavaScriptExecutor(final WebClient webClient) {
52          webClient_ = new WeakReference<>(webClient);
53          jobManagerList_ = new ArrayList<>();
54          shutdown_ = new AtomicBoolean();
55      }
56  
57      /**
58       * Starts the eventLoopThread_.
59       */
60      protected void startThreadIfNeeded() {
61          if (eventLoopThread_ == null) {
62              eventLoopThread_ = new Thread(this, getThreadName());
63              eventLoopThread_.setDaemon(true);
64              eventLoopThread_.start();
65          }
66      }
67  
68      /**
69       * Defines the thread name; overload if needed.
70       * @return the name of the js executor thread
71       */
72      protected String getThreadName() {
73          if (shutdown_.get()) {
74              return "Stopped JS executor for " + webClient_.get();
75          }
76          return "JS executor for " + webClient_.get();
77      }
78  
79      @SuppressWarnings("deprecation")
80      private void killThread() {
81          if (eventLoopThread_ == null) {
82              return;
83          }
84  
85          try {
86              eventLoopThread_.interrupt();
87              eventLoopThread_.join(10_000);
88          }
89          catch (final InterruptedException e) {
90              LOG.warn("InterruptedException while waiting for the eventLoop thread to join", e);
91  
92              // restore interrupted status
93              Thread.currentThread().interrupt();
94          }
95  
96          if (eventLoopThread_.isAlive()) {
97              if (LOG.isWarnEnabled()) {
98                  LOG.warn("Event loop thread "
99                          + eventLoopThread_.getName()
100                         + " still alive at "
101                         + System.currentTimeMillis());
102                 LOG.warn("Event loop thread will be stopped");
103             }
104 
105             // Stop the thread
106             try {
107                 eventLoopThread_.stop();
108             }
109             catch (final Exception e) {
110                 LOG.warn("JS thread did not interrupt after 10s, maybe there is an endless loop."
111                         + "Please consider setting an JavaScriptTimeout for the WebClient.", e);
112             }
113         }
114     }
115 
116     /**
117      * Returns the JobExecutor corresponding to the earliest job.
118      * @return the JobExectuor with the earliest job.
119      */
120     protected JavaScriptJobManager getJobManagerWithEarliestJob() {
121         JavaScriptJobManager javaScriptJobManager = null;
122         JavaScriptJob earliestJob = null;
123 
124         synchronized (jobManagerList_) {
125             // iterate over the list and find the earliest job to run.
126             for (final WeakReference<JavaScriptJobManager> weakReference : jobManagerList_) {
127                 final JavaScriptJobManager jobManager = weakReference.get();
128                 if (jobManager != null) {
129                     final JavaScriptJob newJob = jobManager.getEarliestJob();
130                     if (newJob != null && (earliestJob == null || earliestJob.compareTo(newJob) > 0)) {
131                         earliestJob = newJob;
132                         javaScriptJobManager = jobManager;
133                     }
134                 }
135             }
136         }
137         return javaScriptJobManager;
138     }
139 
140     /** Runs the eventLoop. */
141     @Override
142     public void run() {
143         final boolean trace = LOG.isTraceEnabled();
144         // this has to be a multiple of 10ms
145         // otherwise the VM has to fight with the OS to get such small periods
146         final long sleepInterval = 10;
147         while (!shutdown_.get() && !Thread.currentThread().isInterrupted() && webClient_.get() != null) {
148             final JavaScriptJobManager jobManager = getJobManagerWithEarliestJob();
149 
150             if (jobManager != null) {
151                 final JavaScriptJob earliestJob = jobManager.getEarliestJob();
152                 if (earliestJob != null) {
153                     final long waitTime = earliestJob.getTargetExecutionTime() - System.currentTimeMillis();
154 
155                     // do we have to execute the earliest job
156                     if (waitTime < 1) {
157                         // execute the earliest job
158                         if (trace) {
159                             LOG.trace("started executing job at " + System.currentTimeMillis());
160                         }
161                         jobManager.runSingleJob(earliestJob);
162                         if (trace) {
163                             LOG.trace("stopped executing job at " + System.currentTimeMillis());
164                         }
165 
166                         // job is done, have a look for another one
167                         continue;
168                     }
169                 }
170             }
171 
172             // check for cancel
173             if (shutdown_.get() || Thread.currentThread().isInterrupted() || webClient_.get() == null) {
174                 break;
175             }
176 
177             // nothing to do, let's sleep a bit
178             try {
179                 Thread.sleep(sleepInterval);
180             }
181             catch (final InterruptedException e) {
182                 // restore interrupted status
183                 Thread.currentThread().interrupt();
184 
185                 break;
186             }
187         }
188     }
189 
190     /**
191      * Register a window with the eventLoop.
192      * @param newWindow the new web window
193      */
194     @Override
195     public void addWindow(final WebWindow newWindow) {
196         final JavaScriptJobManager jobManager = newWindow.getJobManager();
197         if (jobManager != null) {
198             updateJobMangerList(jobManager);
199             startThreadIfNeeded();
200         }
201     }
202 
203     private void updateJobMangerList(final JavaScriptJobManager newJobManager) {
204         final List<WeakReference<JavaScriptJobManager>> managers = new ArrayList<>(jobManagerList_.size());
205         synchronized (jobManagerList_) {
206             for (final WeakReference<JavaScriptJobManager> weakReference : jobManagerList_) {
207                 final JavaScriptJobManager manager = weakReference.get();
208                 if (newJobManager == manager) {
209                     return;
210                 }
211                 if (null != weakReference.get()) {
212                     managers.add(weakReference);
213                 }
214             }
215 
216             managers.add(new WeakReference<>(newJobManager));
217 
218             jobManagerList_.clear();
219             jobManagerList_.addAll(managers);
220         }
221     }
222 
223     /** Notes that this thread has been shutdown. */
224     @Override
225     public void shutdown() {
226         shutdown_.set(true);
227         killThread();
228 
229         webClient_.clear();
230         synchronized (jobManagerList_) {
231             jobManagerList_.clear();
232         }
233     }
234 }