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