1 /*
2  * This program is free software; you can redistribute it and/or
3  * modify it under the terms of the GNU General Public License
4  * as published by the Free Software Foundation; either version 2
5  * of the License, or (at your option) any later version.
6  *
7  * This program is distributed in the hope that it will be useful,
8  * but WITHOUT ANY WARRANTY; without even the implied warranty of
9  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10  * GNU General Public License for more details.
11  *
12  * You should have received a copy of the GNU General Public License
13  * along with this program; if not, write to the Free Software Foundation,
14  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
15  *
16  * The Original Code is Copyright (C) 2009 Blender Foundation.
17  * All rights reserved.
18  */
19 
20 /** \file
21  * \ingroup wm
22  *
23  * Threaded job manager (high level job access).
24  */
25 
26 #include <string.h>
27 
28 #include "DNA_windowmanager_types.h"
29 
30 #include "MEM_guardedalloc.h"
31 
32 #include "BLI_blenlib.h"
33 #include "BLI_threads.h"
34 #include "BLI_utildefines.h"
35 
36 #include "BKE_context.h"
37 #include "BKE_global.h"
38 #include "BKE_sequencer.h"
39 
40 #include "WM_api.h"
41 #include "WM_types.h"
42 #include "wm.h"
43 #include "wm_event_types.h"
44 
45 #include "PIL_time.h"
46 
47 /*
48  * Add new job
49  * - register in WM
50  * - configure callbacks
51  *
52  * Start or re-run job
53  * - if job running
54  *   - signal job to end
55  *   - add timer notifier to verify when it has ended, to start it
56  * - else
57  *   - start job
58  *   - add timer notifier to handle progress
59  *
60  * Stop job
61  * - signal job to end
62  * on end, job will tag itself as sleeping
63  *
64  * Remove job
65  * - signal job to end
66  * on end, job will remove itself
67  *
68  * When job is done:
69  * - it puts timer to sleep (or removes?)
70  */
71 
72 struct wmJob {
73   struct wmJob *next, *prev;
74 
75   /** Job originating from, keep track of this when deleting windows */
76   wmWindow *win;
77 
78   /** Should store entire own context, for start, update, free */
79   void *customdata;
80   /**
81    * To prevent cpu overhead, use this one which only gets called when job really starts.
82    * Executed in main thread.
83    */
84   void (*initjob)(void *);
85   /**
86    * This performs the actual parallel work.
87    * Executed in worker thread(s).
88    */
89   wm_jobs_start_callback startjob;
90   /**
91    * Called if thread defines so (see `do_update` flag), and max once per timer step.
92    * Executed in main thread.
93    */
94   void (*update)(void *);
95   /**
96    * Free callback (typically for customdata).
97    * Executed in main thread.
98    */
99   void (*free)(void *);
100   /**
101    * Called when job is stopped.
102    * Executed in main thread.
103    */
104   void (*endjob)(void *);
105 
106   /** Running jobs each have own timer */
107   double timestep;
108   wmTimer *wt;
109   /** Only start job after specified time delay */
110   double start_delay_time;
111   /** The notifier event timers should send */
112   unsigned int note, endnote;
113 
114   /* internal */
115   void *owner;
116   int flag;
117   short suspended, running, ready, do_update, stop, job_type;
118   float progress;
119 
120   /** For display in header, identification */
121   char name[128];
122 
123   /** Once running, we store this separately */
124   void *run_customdata;
125   void (*run_free)(void *);
126 
127   /** We use BLI_threads api, but per job only 1 thread runs */
128   ListBase threads;
129 
130   double start_time;
131 
132   /** Ticket mutex for main thread locking while some job accesses
133    * data that the main thread might modify at the same time */
134   TicketMutex *main_thread_mutex;
135 };
136 
137 /* Main thread locking */
138 
WM_job_main_thread_lock_acquire(wmJob * wm_job)139 void WM_job_main_thread_lock_acquire(wmJob *wm_job)
140 {
141   BLI_ticket_mutex_lock(wm_job->main_thread_mutex);
142 }
143 
WM_job_main_thread_lock_release(wmJob * wm_job)144 void WM_job_main_thread_lock_release(wmJob *wm_job)
145 {
146   BLI_ticket_mutex_unlock(wm_job->main_thread_mutex);
147 }
148 
wm_job_main_thread_yield(wmJob * wm_job)149 static void wm_job_main_thread_yield(wmJob *wm_job)
150 {
151   /* unlock and lock the ticket mutex. because it's a fair mutex any job that
152    * is waiting to acquire the lock will get it first, before we can lock */
153   BLI_ticket_mutex_unlock(wm_job->main_thread_mutex);
154   BLI_ticket_mutex_lock(wm_job->main_thread_mutex);
155 }
156 
157 /**
158  * Finds if type or owner, compare for it, otherwise any matching job.
159  */
wm_job_find(wmWindowManager * wm,void * owner,const int job_type)160 static wmJob *wm_job_find(wmWindowManager *wm, void *owner, const int job_type)
161 {
162   if (owner && job_type) {
163     LISTBASE_FOREACH (wmJob *, wm_job, &wm->jobs) {
164       if (wm_job->owner == owner && wm_job->job_type == job_type) {
165         return wm_job;
166       }
167     }
168   }
169   else if (owner) {
170     LISTBASE_FOREACH (wmJob *, wm_job, &wm->jobs) {
171       if (wm_job->owner == owner) {
172         return wm_job;
173       }
174     }
175   }
176   else if (job_type) {
177     LISTBASE_FOREACH (wmJob *, wm_job, &wm->jobs) {
178       if (wm_job->job_type == job_type) {
179         return wm_job;
180       }
181     }
182   }
183 
184   return NULL;
185 }
186 
187 /* ******************* public API ***************** */
188 
189 /**
190  * \return current job or adds new job, but doesn't run it.
191  *
192  * \note every owner only gets a single job,
193  * adding a new one will stop running job and when stopped it starts the new one.
194  */
WM_jobs_get(wmWindowManager * wm,wmWindow * win,void * owner,const char * name,int flag,int job_type)195 wmJob *WM_jobs_get(
196     wmWindowManager *wm, wmWindow *win, void *owner, const char *name, int flag, int job_type)
197 {
198   wmJob *wm_job = wm_job_find(wm, owner, job_type);
199 
200   if (wm_job == NULL) {
201     wm_job = MEM_callocN(sizeof(wmJob), "new job");
202 
203     BLI_addtail(&wm->jobs, wm_job);
204     wm_job->win = win;
205     wm_job->owner = owner;
206     wm_job->flag = flag;
207     wm_job->job_type = job_type;
208     BLI_strncpy(wm_job->name, name, sizeof(wm_job->name));
209 
210     wm_job->main_thread_mutex = BLI_ticket_mutex_alloc();
211     WM_job_main_thread_lock_acquire(wm_job);
212   }
213   /* else: a running job, be careful */
214 
215   /* prevent creating a job with an invalid type */
216   BLI_assert(wm_job->job_type != WM_JOB_TYPE_ANY);
217 
218   return wm_job;
219 }
220 
221 /* returns true if job runs, for UI (progress) indicators */
WM_jobs_test(wmWindowManager * wm,void * owner,int job_type)222 bool WM_jobs_test(wmWindowManager *wm, void *owner, int job_type)
223 {
224   wmJob *wm_job;
225 
226   /* job can be running or about to run (suspended) */
227   for (wm_job = wm->jobs.first; wm_job; wm_job = wm_job->next) {
228     if (wm_job->owner == owner) {
229       if (job_type == WM_JOB_TYPE_ANY || (wm_job->job_type == job_type)) {
230         if (wm_job->running || wm_job->suspended) {
231           return true;
232         }
233       }
234     }
235   }
236 
237   return false;
238 }
239 
WM_jobs_progress(wmWindowManager * wm,void * owner)240 float WM_jobs_progress(wmWindowManager *wm, void *owner)
241 {
242   wmJob *wm_job = wm_job_find(wm, owner, WM_JOB_TYPE_ANY);
243 
244   if (wm_job && wm_job->flag & WM_JOB_PROGRESS) {
245     return wm_job->progress;
246   }
247 
248   return 0.0;
249 }
250 
wm_jobs_update_progress_bars(wmWindowManager * wm)251 static void wm_jobs_update_progress_bars(wmWindowManager *wm)
252 {
253   float total_progress = 0.f;
254   float jobs_progress = 0;
255 
256   LISTBASE_FOREACH (wmJob *, wm_job, &wm->jobs) {
257     if (wm_job->threads.first && !wm_job->ready) {
258       if (wm_job->flag & WM_JOB_PROGRESS) {
259         /* accumulate global progress for running jobs */
260         jobs_progress++;
261         total_progress += wm_job->progress;
262       }
263     }
264   }
265 
266   /* if there are running jobs, set the global progress indicator */
267   if (jobs_progress > 0) {
268     wmWindow *win;
269     float progress = total_progress / (float)jobs_progress;
270 
271     for (win = wm->windows.first; win; win = win->next) {
272       WM_progress_set(win, progress);
273     }
274   }
275   else {
276     wmWindow *win;
277 
278     for (win = wm->windows.first; win; win = win->next) {
279       WM_progress_clear(win);
280     }
281   }
282 }
283 
284 /* time that job started */
WM_jobs_starttime(wmWindowManager * wm,void * owner)285 double WM_jobs_starttime(wmWindowManager *wm, void *owner)
286 {
287   wmJob *wm_job = wm_job_find(wm, owner, WM_JOB_TYPE_ANY);
288 
289   if (wm_job && wm_job->flag & WM_JOB_PROGRESS) {
290     return wm_job->start_time;
291   }
292 
293   return 0;
294 }
295 
WM_jobs_name(wmWindowManager * wm,void * owner)296 char *WM_jobs_name(wmWindowManager *wm, void *owner)
297 {
298   wmJob *wm_job = wm_job_find(wm, owner, WM_JOB_TYPE_ANY);
299 
300   if (wm_job) {
301     return wm_job->name;
302   }
303 
304   return NULL;
305 }
306 
WM_jobs_customdata(wmWindowManager * wm,void * owner)307 void *WM_jobs_customdata(wmWindowManager *wm, void *owner)
308 {
309   wmJob *wm_job = wm_job_find(wm, owner, WM_JOB_TYPE_ANY);
310 
311   if (wm_job) {
312     return WM_jobs_customdata_get(wm_job);
313   }
314 
315   return NULL;
316 }
317 
WM_jobs_customdata_from_type(wmWindowManager * wm,int job_type)318 void *WM_jobs_customdata_from_type(wmWindowManager *wm, int job_type)
319 {
320   wmJob *wm_job = wm_job_find(wm, NULL, job_type);
321 
322   if (wm_job) {
323     return WM_jobs_customdata_get(wm_job);
324   }
325 
326   return NULL;
327 }
328 
WM_jobs_is_running(wmJob * wm_job)329 bool WM_jobs_is_running(wmJob *wm_job)
330 {
331   return wm_job->running;
332 }
333 
WM_jobs_is_stopped(wmWindowManager * wm,void * owner)334 bool WM_jobs_is_stopped(wmWindowManager *wm, void *owner)
335 {
336   wmJob *wm_job = wm_job_find(wm, owner, WM_JOB_TYPE_ANY);
337   return wm_job ? wm_job->stop : true; /* XXX to be redesigned properly. */
338 }
339 
WM_jobs_customdata_get(wmJob * wm_job)340 void *WM_jobs_customdata_get(wmJob *wm_job)
341 {
342   if (!wm_job->customdata) {
343     return wm_job->run_customdata;
344   }
345   return wm_job->customdata;
346 }
347 
WM_jobs_customdata_set(wmJob * wm_job,void * customdata,void (* free)(void *))348 void WM_jobs_customdata_set(wmJob *wm_job, void *customdata, void (*free)(void *))
349 {
350   /* pending job? just free */
351   if (wm_job->customdata) {
352     wm_job->free(wm_job->customdata);
353   }
354 
355   wm_job->customdata = customdata;
356   wm_job->free = free;
357 
358   if (wm_job->running) {
359     /* signal job to end */
360     wm_job->stop = true;
361   }
362 }
363 
WM_jobs_timer(wmJob * wm_job,double timestep,unsigned int note,unsigned int endnote)364 void WM_jobs_timer(wmJob *wm_job, double timestep, unsigned int note, unsigned int endnote)
365 {
366   wm_job->timestep = timestep;
367   wm_job->note = note;
368   wm_job->endnote = endnote;
369 }
370 
WM_jobs_delay_start(wmJob * wm_job,double delay_time)371 void WM_jobs_delay_start(wmJob *wm_job, double delay_time)
372 {
373   wm_job->start_delay_time = delay_time;
374 }
375 
WM_jobs_callbacks(wmJob * wm_job,wm_jobs_start_callback startjob,void (* initjob)(void *),void (* update)(void *),void (* endjob)(void *))376 void WM_jobs_callbacks(wmJob *wm_job,
377                        wm_jobs_start_callback startjob,
378                        void (*initjob)(void *),
379                        void (*update)(void *),
380                        void (*endjob)(void *))
381 {
382   wm_job->startjob = startjob;
383   wm_job->initjob = initjob;
384   wm_job->update = update;
385   wm_job->endjob = endjob;
386 }
387 
do_job_thread(void * job_v)388 static void *do_job_thread(void *job_v)
389 {
390   wmJob *wm_job = job_v;
391 
392   BLI_thread_put_thread_on_fast_node();
393   wm_job->startjob(wm_job->run_customdata, &wm_job->stop, &wm_job->do_update, &wm_job->progress);
394   wm_job->ready = true;
395 
396   return NULL;
397 }
398 
399 /* don't allow same startjob to be executed twice */
wm_jobs_test_suspend_stop(wmWindowManager * wm,wmJob * test)400 static void wm_jobs_test_suspend_stop(wmWindowManager *wm, wmJob *test)
401 {
402   wmJob *wm_job;
403   bool suspend = false;
404 
405   /* job added with suspend flag, we wait 1 timer step before activating it */
406   if (test->start_delay_time > 0.0) {
407     suspend = true;
408     test->start_delay_time = 0.0;
409   }
410   else {
411     /* check other jobs */
412     for (wm_job = wm->jobs.first; wm_job; wm_job = wm_job->next) {
413       /* obvious case, no test needed */
414       if (wm_job == test || !wm_job->running) {
415         continue;
416       }
417 
418       /* if new job is not render, then check for same startjob */
419       if (0 == (test->flag & WM_JOB_EXCL_RENDER)) {
420         if (wm_job->startjob != test->startjob) {
421           continue;
422         }
423       }
424 
425       /* if new job is render, any render job should be stopped */
426       if (test->flag & WM_JOB_EXCL_RENDER) {
427         if (0 == (wm_job->flag & WM_JOB_EXCL_RENDER)) {
428           continue;
429         }
430       }
431 
432       suspend = true;
433 
434       /* if this job has higher priority, stop others */
435       if (test->flag & WM_JOB_PRIORITY) {
436         wm_job->stop = true;
437         // printf("job stopped: %s\n", wm_job->name);
438       }
439     }
440   }
441 
442   /* Possible suspend ourselves, waiting for other jobs, or de-suspend. */
443   test->suspended = suspend;
444 #if 0
445   if (suspend) {
446     printf("job suspended: %s\n", test->name);
447   }
448 #endif
449 }
450 
451 /**
452  * if job running, the same owner gave it a new job.
453  * if different owner starts existing startjob, it suspends itself
454  */
WM_jobs_start(wmWindowManager * wm,wmJob * wm_job)455 void WM_jobs_start(wmWindowManager *wm, wmJob *wm_job)
456 {
457   if (wm_job->running) {
458     /* signal job to end and restart */
459     wm_job->stop = true;
460     // printf("job started a running job, ending... %s\n", wm_job->name);
461   }
462   else {
463 
464     if (wm_job->customdata && wm_job->startjob) {
465       const double timestep = (wm_job->start_delay_time > 0.0) ? wm_job->start_delay_time :
466                                                                  wm_job->timestep;
467 
468       wm_jobs_test_suspend_stop(wm, wm_job);
469 
470       if (wm_job->suspended == false) {
471         /* copy to ensure proper free in end */
472         wm_job->run_customdata = wm_job->customdata;
473         wm_job->run_free = wm_job->free;
474         wm_job->free = NULL;
475         wm_job->customdata = NULL;
476         wm_job->running = true;
477 
478         if (wm_job->initjob) {
479           wm_job->initjob(wm_job->run_customdata);
480         }
481 
482         wm_job->stop = false;
483         wm_job->ready = false;
484         wm_job->progress = 0.0;
485 
486         // printf("job started: %s\n", wm_job->name);
487 
488         BLI_threadpool_init(&wm_job->threads, do_job_thread, 1);
489         BLI_threadpool_insert(&wm_job->threads, wm_job);
490       }
491 
492       /* restarted job has timer already */
493       if (wm_job->wt && (wm_job->wt->timestep > timestep)) {
494         WM_event_remove_timer(wm, wm_job->win, wm_job->wt);
495         wm_job->wt = WM_event_add_timer(wm, wm_job->win, TIMERJOBS, timestep);
496       }
497       if (wm_job->wt == NULL) {
498         wm_job->wt = WM_event_add_timer(wm, wm_job->win, TIMERJOBS, timestep);
499       }
500 
501       wm_job->start_time = PIL_check_seconds_timer();
502     }
503     else {
504       printf("job fails, not initialized\n");
505     }
506   }
507 }
508 
wm_job_free(wmWindowManager * wm,wmJob * wm_job)509 static void wm_job_free(wmWindowManager *wm, wmJob *wm_job)
510 {
511   BLI_remlink(&wm->jobs, wm_job);
512   WM_job_main_thread_lock_release(wm_job);
513   BLI_ticket_mutex_free(wm_job->main_thread_mutex);
514   MEM_freeN(wm_job);
515 }
516 
517 /* stop job, end thread, free data completely */
wm_jobs_kill_job(wmWindowManager * wm,wmJob * wm_job)518 static void wm_jobs_kill_job(wmWindowManager *wm, wmJob *wm_job)
519 {
520   bool update_progress = (wm_job->flag & WM_JOB_PROGRESS) != 0;
521 
522   if (wm_job->running) {
523     /* signal job to end */
524     wm_job->stop = true;
525 
526     WM_job_main_thread_lock_release(wm_job);
527     BLI_threadpool_end(&wm_job->threads);
528     WM_job_main_thread_lock_acquire(wm_job);
529 
530     if (wm_job->endjob) {
531       wm_job->endjob(wm_job->run_customdata);
532     }
533   }
534 
535   if (wm_job->wt) {
536     WM_event_remove_timer(wm, wm_job->win, wm_job->wt);
537   }
538   if (wm_job->customdata) {
539     wm_job->free(wm_job->customdata);
540   }
541   if (wm_job->run_customdata) {
542     wm_job->run_free(wm_job->run_customdata);
543   }
544 
545   /* remove wm_job */
546   wm_job_free(wm, wm_job);
547 
548   /* Update progress bars in windows. */
549   if (update_progress) {
550     wm_jobs_update_progress_bars(wm);
551   }
552 }
553 
554 /* wait until every job ended */
WM_jobs_kill_all(wmWindowManager * wm)555 void WM_jobs_kill_all(wmWindowManager *wm)
556 {
557   wmJob *wm_job;
558 
559   while ((wm_job = wm->jobs.first)) {
560     wm_jobs_kill_job(wm, wm_job);
561   }
562 
563   /* This job will be automatically restarted */
564   BKE_sequencer_prefetch_stop_all();
565 }
566 
567 /* wait until every job ended, except for one owner (used in undo to keep screen job alive) */
WM_jobs_kill_all_except(wmWindowManager * wm,void * owner)568 void WM_jobs_kill_all_except(wmWindowManager *wm, void *owner)
569 {
570   wmJob *wm_job, *next_job;
571 
572   for (wm_job = wm->jobs.first; wm_job; wm_job = next_job) {
573     next_job = wm_job->next;
574 
575     if (wm_job->owner != owner) {
576       wm_jobs_kill_job(wm, wm_job);
577     }
578   }
579 }
580 
WM_jobs_kill_type(struct wmWindowManager * wm,void * owner,int job_type)581 void WM_jobs_kill_type(struct wmWindowManager *wm, void *owner, int job_type)
582 {
583   wmJob *wm_job, *next_job;
584 
585   for (wm_job = wm->jobs.first; wm_job; wm_job = next_job) {
586     next_job = wm_job->next;
587 
588     if (!owner || wm_job->owner == owner) {
589       if (job_type == WM_JOB_TYPE_ANY || wm_job->job_type == job_type) {
590         wm_jobs_kill_job(wm, wm_job);
591       }
592     }
593   }
594 }
595 
596 /* signal job(s) from this owner or callback to stop, timer is required to get handled */
WM_jobs_stop(wmWindowManager * wm,void * owner,void * startjob)597 void WM_jobs_stop(wmWindowManager *wm, void *owner, void *startjob)
598 {
599   wmJob *wm_job;
600 
601   for (wm_job = wm->jobs.first; wm_job; wm_job = wm_job->next) {
602     if (wm_job->owner == owner || wm_job->startjob == startjob) {
603       if (wm_job->running) {
604         wm_job->stop = true;
605       }
606     }
607   }
608 }
609 
610 /* actually terminate thread and job timer */
WM_jobs_kill(wmWindowManager * wm,void * owner,void (* startjob)(void *,short int *,short int *,float *))611 void WM_jobs_kill(wmWindowManager *wm,
612                   void *owner,
613                   void (*startjob)(void *, short int *, short int *, float *))
614 {
615   wmJob *wm_job;
616 
617   wm_job = wm->jobs.first;
618   while (wm_job) {
619     if (wm_job->owner == owner || wm_job->startjob == startjob) {
620       wmJob *wm_job_kill = wm_job;
621       wm_job = wm_job->next;
622       wm_jobs_kill_job(wm, wm_job_kill);
623     }
624     else {
625       wm_job = wm_job->next;
626     }
627   }
628 }
629 
630 /* kill job entirely, also removes timer itself */
wm_jobs_timer_ended(wmWindowManager * wm,wmTimer * wt)631 void wm_jobs_timer_ended(wmWindowManager *wm, wmTimer *wt)
632 {
633   wmJob *wm_job;
634 
635   for (wm_job = wm->jobs.first; wm_job; wm_job = wm_job->next) {
636     if (wm_job->wt == wt) {
637       wm_jobs_kill_job(wm, wm_job);
638       return;
639     }
640   }
641 }
642 
643 /* hardcoded to event TIMERJOBS */
wm_jobs_timer(wmWindowManager * wm,wmTimer * wt)644 void wm_jobs_timer(wmWindowManager *wm, wmTimer *wt)
645 {
646   wmJob *wm_job, *wm_jobnext;
647 
648   for (wm_job = wm->jobs.first; wm_job; wm_job = wm_jobnext) {
649     wm_jobnext = wm_job->next;
650 
651     if (wm_job->wt == wt) {
652 
653       /* running threads */
654       if (wm_job->threads.first) {
655 
656         /* let threads get temporary lock over main thread if needed */
657         wm_job_main_thread_yield(wm_job);
658 
659         /* always call note and update when ready */
660         if (wm_job->do_update || wm_job->ready) {
661           if (wm_job->update) {
662             wm_job->update(wm_job->run_customdata);
663           }
664           if (wm_job->note) {
665             WM_event_add_notifier_ex(wm, wm_job->win, wm_job->note, NULL);
666           }
667 
668           if (wm_job->flag & WM_JOB_PROGRESS) {
669             WM_event_add_notifier_ex(wm, wm_job->win, NC_WM | ND_JOB, NULL);
670           }
671           wm_job->do_update = false;
672         }
673 
674         if (wm_job->ready) {
675           if (wm_job->endjob) {
676             wm_job->endjob(wm_job->run_customdata);
677           }
678 
679           /* free own data */
680           wm_job->run_free(wm_job->run_customdata);
681           wm_job->run_customdata = NULL;
682           wm_job->run_free = NULL;
683 
684 #if 0
685           if (wm_job->stop) {
686             printf("job ready but stopped %s\n", wm_job->name);
687           }
688           else {
689             printf("job finished %s\n", wm_job->name);
690           }
691 #endif
692 
693           if (G.debug & G_DEBUG_JOBS) {
694             printf("Job '%s' finished in %f seconds\n",
695                    wm_job->name,
696                    PIL_check_seconds_timer() - wm_job->start_time);
697           }
698 
699           wm_job->running = false;
700 
701           WM_job_main_thread_lock_release(wm_job);
702           BLI_threadpool_end(&wm_job->threads);
703           WM_job_main_thread_lock_acquire(wm_job);
704 
705           if (wm_job->endnote) {
706             WM_event_add_notifier_ex(wm, wm_job->win, wm_job->endnote, NULL);
707           }
708 
709           WM_event_add_notifier_ex(wm, wm_job->win, NC_WM | ND_JOB, NULL);
710 
711           /* new job added for wm_job? */
712           if (wm_job->customdata) {
713             // printf("job restarted with new data %s\n", wm_job->name);
714             WM_jobs_start(wm, wm_job);
715           }
716           else {
717             WM_event_remove_timer(wm, wm_job->win, wm_job->wt);
718             wm_job->wt = NULL;
719 
720             /* remove wm_job */
721             wm_job_free(wm, wm_job);
722           }
723         }
724       }
725       else if (wm_job->suspended) {
726         WM_jobs_start(wm, wm_job);
727       }
728     }
729   }
730 
731   /* Update progress bars in windows. */
732   wm_jobs_update_progress_bars(wm);
733 }
734 
WM_jobs_has_running(wmWindowManager * wm)735 bool WM_jobs_has_running(wmWindowManager *wm)
736 {
737   wmJob *wm_job;
738 
739   for (wm_job = wm->jobs.first; wm_job; wm_job = wm_job->next) {
740     if (wm_job->running) {
741       return true;
742     }
743   }
744 
745   return false;
746 }
747