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