1
+ """starts a long-running process that watches the file system and
2
+ automatically execute tasks when file dependencies change"""
3
+
4
+ import os
5
+ import time
6
+ import sys
7
+ from multiprocess import Process
8
+ from subprocess import call
9
+
10
+ from doit .exceptions import InvalidCommand
11
+ from doit .cmdparse import CmdParse
12
+ from doit .cmd_base import tasks_and_deps_iter
13
+ from doit .cmd_base import DoitCmdBase
14
+ from doit .cmd_run import opt_verbosity , Run
15
+
16
+ from .filewatch import FileModifyWatcher
17
+
18
+ opt_reporter = {
19
+ 'name' :'reporter' ,
20
+ 'short' : None ,
21
+ 'long' : None ,
22
+ 'type' :str ,
23
+ 'default' : 'executed-only' ,
24
+ }
25
+
26
+ opt_success = {
27
+ 'name' :'success_callback' ,
28
+ 'short' : None ,
29
+ 'long' : 'success' ,
30
+ 'type' :str ,
31
+ 'default' : '' ,
32
+ }
33
+
34
+ opt_failure = {
35
+ 'name' :'failure_callback' ,
36
+ 'short' : None ,
37
+ 'long' : 'failure' ,
38
+ 'type' :str ,
39
+ 'default' : '' ,
40
+ }
41
+
42
+
43
+ class Watch (DoitCmdBase ):
44
+ """the main process will never load tasks,
45
+ delegates execution to a forked process.
46
+
47
+ python caches imported modules,
48
+ but using different process we can have dependencies on python
49
+ modules making sure the newest module will be used.
50
+ """
51
+
52
+ doc_purpose = "automatically execute tasks when a dependency changes"
53
+ doc_usage = "[TASK ...]"
54
+ doc_description = None
55
+ execute_tasks = True
56
+
57
+ cmd_options = (opt_verbosity , opt_reporter , opt_success , opt_failure )
58
+
59
+ @staticmethod
60
+ def _find_file_deps (tasks , sel_tasks ):
61
+ """find all file deps
62
+ @param tasks (dict)
63
+ @param sel_tasks(list - str)
64
+ """
65
+ deps = set ()
66
+ for task in tasks_and_deps_iter (tasks , sel_tasks ):
67
+ deps .update (task .file_dep )
68
+ deps .update (task .watch )
69
+ return deps
70
+
71
+
72
+ @staticmethod
73
+ def _dep_changed (watch_files , started , targets ):
74
+ """check if watched files was modified since execution started"""
75
+ for watched in watch_files :
76
+ # assume that changes to targets were done by doit itself
77
+ if watched in targets :
78
+ continue
79
+ if os .stat (watched ).st_mtime > started :
80
+ return True
81
+ return False
82
+
83
+
84
+ @staticmethod
85
+ def _run_callback (result , success_callback , failure_callback ):
86
+ '''run callback if any after task execution'''
87
+ if result == 0 :
88
+ if success_callback :
89
+ call (success_callback , shell = True )
90
+ else :
91
+ if failure_callback :
92
+ call (failure_callback , shell = True )
93
+
94
+
95
+ def run_watch (self , params , args ):
96
+ """Run tasks and wait for file system event
97
+
98
+ This method is executed in a forked process.
99
+ The process is terminated after a single event.
100
+ """
101
+ started = time .time ()
102
+
103
+ # execute tasks using Run Command
104
+ arun = Run (task_loader = self .loader )
105
+ params .add_defaults (CmdParse (arun .get_options ()).parse ([])[0 ])
106
+ try :
107
+ result = arun .execute (params , args )
108
+ # ??? actually tested but coverage doesnt get it...
109
+ except InvalidCommand as err : # pragma: no cover
110
+ sys .stderr .write ("ERROR: %s\n " % str (err ))
111
+ sys .exit (3 )
112
+
113
+ # user custom callbacks for result
114
+ self ._run_callback (result ,
115
+ params .pop ('success_callback' , None ),
116
+ params .pop ('failure_callback' , None ))
117
+
118
+ # get list of files to watch on file system
119
+ watch_files = self ._find_file_deps (arun .control .tasks ,
120
+ arun .control .selected_tasks )
121
+
122
+ # Check for timestamp changes since run started,
123
+ # if change, restart straight away
124
+ if not self ._dep_changed (watch_files , started , arun .control .targets ):
125
+ # set event handler. just terminate process.
126
+ class DoitAutoRun (FileModifyWatcher ):
127
+ def handle_event (self , event ):
128
+ # print("FS EVENT -> {}".format(event))
129
+ sys .exit (result )
130
+ file_watcher = DoitAutoRun (watch_files )
131
+ # kick start watching process
132
+ file_watcher .loop ()
133
+
134
+
135
+ def execute (self , params , args ):
136
+ """loop executing tasks until process is interrupted"""
137
+ while True :
138
+ try :
139
+ proc = Process (target = self .run_watch , args = (params , args ))
140
+ proc .start ()
141
+ proc .join ()
142
+ # if error on given command line, terminate.
143
+ if proc .exitcode == 3 :
144
+ return 3
145
+ except KeyboardInterrupt :
146
+ return 0
0 commit comments