1c5f01b2fSopenharmony_ci#!/usr/bin/env python3
2c5f01b2fSopenharmony_ci
3c5f01b2fSopenharmony_ciimport contextlib
4c5f01b2fSopenharmony_ciimport logging
5c5f01b2fSopenharmony_ciimport os
6c5f01b2fSopenharmony_ciimport re
7c5f01b2fSopenharmony_ciimport shutil
8c5f01b2fSopenharmony_ciimport sys
9c5f01b2fSopenharmony_ciimport subprocess
10c5f01b2fSopenharmony_ci
11c5f01b2fSopenharmony_cifrom datetime import datetime, timedelta
12c5f01b2fSopenharmony_cifrom io import BytesIO
13c5f01b2fSopenharmony_cifrom threading import Lock, Timer
14c5f01b2fSopenharmony_ci
15c5f01b2fSopenharmony_cifrom watchdog.events import FileSystemEventHandler
16c5f01b2fSopenharmony_cifrom watchdog.observers import Observer
17c5f01b2fSopenharmony_ci
18c5f01b2fSopenharmony_cifrom http import HTTPStatus
19c5f01b2fSopenharmony_cifrom http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
20c5f01b2fSopenharmony_ci
21c5f01b2fSopenharmony_ciCONFIG_FILE = 'serve_header.yml'
22c5f01b2fSopenharmony_ciMAKEFILE = 'Makefile'
23c5f01b2fSopenharmony_ciINCLUDE = 'include/nlohmann/'
24c5f01b2fSopenharmony_ciSINGLE_INCLUDE = 'single_include/nlohmann/'
25c5f01b2fSopenharmony_ciHEADER = 'json.hpp'
26c5f01b2fSopenharmony_ci
27c5f01b2fSopenharmony_ciDATETIME_FORMAT = '%Y-%m-%d %H:%M:%S'
28c5f01b2fSopenharmony_ci
29c5f01b2fSopenharmony_ciJSON_VERSION_RE = re.compile(r'\s*#\s*define\s+NLOHMANN_JSON_VERSION_MAJOR\s+')
30c5f01b2fSopenharmony_ci
31c5f01b2fSopenharmony_ciclass ExitHandler(logging.StreamHandler):
32c5f01b2fSopenharmony_ci    def __init__(self, level):
33c5f01b2fSopenharmony_ci        """."""
34c5f01b2fSopenharmony_ci        super().__init__()
35c5f01b2fSopenharmony_ci        self.level = level
36c5f01b2fSopenharmony_ci
37c5f01b2fSopenharmony_ci    def emit(self, record):
38c5f01b2fSopenharmony_ci        if record.levelno >= self.level:
39c5f01b2fSopenharmony_ci            sys.exit(1)
40c5f01b2fSopenharmony_ci
41c5f01b2fSopenharmony_cidef is_project_root(test_dir='.'):
42c5f01b2fSopenharmony_ci    makefile = os.path.join(test_dir, MAKEFILE)
43c5f01b2fSopenharmony_ci    include = os.path.join(test_dir, INCLUDE)
44c5f01b2fSopenharmony_ci    single_include = os.path.join(test_dir, SINGLE_INCLUDE)
45c5f01b2fSopenharmony_ci
46c5f01b2fSopenharmony_ci    return (os.path.exists(makefile)
47c5f01b2fSopenharmony_ci            and os.path.isfile(makefile)
48c5f01b2fSopenharmony_ci            and os.path.exists(include)
49c5f01b2fSopenharmony_ci            and os.path.exists(single_include))
50c5f01b2fSopenharmony_ci
51c5f01b2fSopenharmony_ciclass DirectoryEventBucket:
52c5f01b2fSopenharmony_ci    def __init__(self, callback, delay=1.2, threshold=0.8):
53c5f01b2fSopenharmony_ci        """."""
54c5f01b2fSopenharmony_ci        self.delay = delay
55c5f01b2fSopenharmony_ci        self.threshold = timedelta(seconds=threshold)
56c5f01b2fSopenharmony_ci        self.callback = callback
57c5f01b2fSopenharmony_ci        self.event_dirs = set([])
58c5f01b2fSopenharmony_ci        self.timer = None
59c5f01b2fSopenharmony_ci        self.lock = Lock()
60c5f01b2fSopenharmony_ci
61c5f01b2fSopenharmony_ci    def start_timer(self):
62c5f01b2fSopenharmony_ci        if self.timer is None:
63c5f01b2fSopenharmony_ci            self.timer = Timer(self.delay, self.process_dirs)
64c5f01b2fSopenharmony_ci            self.timer.start()
65c5f01b2fSopenharmony_ci
66c5f01b2fSopenharmony_ci    def process_dirs(self):
67c5f01b2fSopenharmony_ci        result_dirs = []
68c5f01b2fSopenharmony_ci        event_dirs = set([])
69c5f01b2fSopenharmony_ci        with self.lock:
70c5f01b2fSopenharmony_ci            self.timer = None
71c5f01b2fSopenharmony_ci            while self.event_dirs:
72c5f01b2fSopenharmony_ci                time, event_dir = self.event_dirs.pop()
73c5f01b2fSopenharmony_ci                delta = datetime.now() - time
74c5f01b2fSopenharmony_ci                if delta < self.threshold:
75c5f01b2fSopenharmony_ci                    event_dirs.add((time, event_dir))
76c5f01b2fSopenharmony_ci                else:
77c5f01b2fSopenharmony_ci                    result_dirs.append(event_dir)
78c5f01b2fSopenharmony_ci            self.event_dirs = event_dirs
79c5f01b2fSopenharmony_ci            if result_dirs:
80c5f01b2fSopenharmony_ci                self.callback(os.path.commonpath(result_dirs))
81c5f01b2fSopenharmony_ci            if self.event_dirs:
82c5f01b2fSopenharmony_ci                self.start_timer()
83c5f01b2fSopenharmony_ci
84c5f01b2fSopenharmony_ci    def add_dir(self, path):
85c5f01b2fSopenharmony_ci        with self.lock:
86c5f01b2fSopenharmony_ci            # add path to the set of event_dirs if it is not a sibling of
87c5f01b2fSopenharmony_ci            # a directory already in the set
88c5f01b2fSopenharmony_ci            if not any(os.path.commonpath([path, event_dir]) == event_dir
89c5f01b2fSopenharmony_ci               for (_, event_dir) in self.event_dirs):
90c5f01b2fSopenharmony_ci                self.event_dirs.add((datetime.now(), path))
91c5f01b2fSopenharmony_ci                if self.timer is None:
92c5f01b2fSopenharmony_ci                    self.start_timer()
93c5f01b2fSopenharmony_ci
94c5f01b2fSopenharmony_ciclass WorkTree:
95c5f01b2fSopenharmony_ci    make_command = 'make'
96c5f01b2fSopenharmony_ci
97c5f01b2fSopenharmony_ci    def __init__(self, root_dir, tree_dir):
98c5f01b2fSopenharmony_ci        """."""
99c5f01b2fSopenharmony_ci        self.root_dir = root_dir
100c5f01b2fSopenharmony_ci        self.tree_dir = tree_dir
101c5f01b2fSopenharmony_ci        self.rel_dir = os.path.relpath(tree_dir, root_dir)
102c5f01b2fSopenharmony_ci        self.name = os.path.basename(tree_dir)
103c5f01b2fSopenharmony_ci        self.include_dir = os.path.abspath(os.path.join(tree_dir, INCLUDE))
104c5f01b2fSopenharmony_ci        self.header = os.path.abspath(os.path.join(tree_dir, SINGLE_INCLUDE, HEADER))
105c5f01b2fSopenharmony_ci        self.rel_header = os.path.relpath(self.header, root_dir)
106c5f01b2fSopenharmony_ci        self.dirty = True
107c5f01b2fSopenharmony_ci        self.build_count = 0
108c5f01b2fSopenharmony_ci        t = os.path.getmtime(self.header)
109c5f01b2fSopenharmony_ci        t = datetime.fromtimestamp(t)
110c5f01b2fSopenharmony_ci        self.build_time = t.strftime(DATETIME_FORMAT)
111c5f01b2fSopenharmony_ci
112c5f01b2fSopenharmony_ci    def __hash__(self):
113c5f01b2fSopenharmony_ci        """."""
114c5f01b2fSopenharmony_ci        return hash((self.tree_dir))
115c5f01b2fSopenharmony_ci
116c5f01b2fSopenharmony_ci    def __eq__(self, other):
117c5f01b2fSopenharmony_ci        """."""
118c5f01b2fSopenharmony_ci        if not isinstance(other, type(self)):
119c5f01b2fSopenharmony_ci            return NotImplemented
120c5f01b2fSopenharmony_ci        return self.tree_dir == other.tree_dir
121c5f01b2fSopenharmony_ci
122c5f01b2fSopenharmony_ci    def update_dirty(self, path):
123c5f01b2fSopenharmony_ci        if self.dirty:
124c5f01b2fSopenharmony_ci            return
125c5f01b2fSopenharmony_ci
126c5f01b2fSopenharmony_ci        path = os.path.abspath(path)
127c5f01b2fSopenharmony_ci        if os.path.commonpath([path, self.include_dir]) == self.include_dir:
128c5f01b2fSopenharmony_ci            logging.info(f'{self.name}: working tree marked dirty')
129c5f01b2fSopenharmony_ci            self.dirty = True
130c5f01b2fSopenharmony_ci
131c5f01b2fSopenharmony_ci    def amalgamate_header(self):
132c5f01b2fSopenharmony_ci        if not self.dirty:
133c5f01b2fSopenharmony_ci            return
134c5f01b2fSopenharmony_ci
135c5f01b2fSopenharmony_ci        mtime = os.path.getmtime(self.header)
136c5f01b2fSopenharmony_ci        subprocess.run([WorkTree.make_command, 'amalgamate'], cwd=self.tree_dir,
137c5f01b2fSopenharmony_ci                       stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
138c5f01b2fSopenharmony_ci        if mtime == os.path.getmtime(self.header):
139c5f01b2fSopenharmony_ci            logging.info(f'{self.name}: no changes')
140c5f01b2fSopenharmony_ci        else:
141c5f01b2fSopenharmony_ci            self.build_count += 1
142c5f01b2fSopenharmony_ci            self.build_time = datetime.now().strftime(DATETIME_FORMAT)
143c5f01b2fSopenharmony_ci            logging.info(f'{self.name}: header amalgamated (build count {self.build_count})')
144c5f01b2fSopenharmony_ci
145c5f01b2fSopenharmony_ci        self.dirty = False
146c5f01b2fSopenharmony_ci
147c5f01b2fSopenharmony_ciclass WorkTrees(FileSystemEventHandler):
148c5f01b2fSopenharmony_ci    def __init__(self, root_dir):
149c5f01b2fSopenharmony_ci        """."""
150c5f01b2fSopenharmony_ci        super().__init__()
151c5f01b2fSopenharmony_ci        self.root_dir = root_dir
152c5f01b2fSopenharmony_ci        self.trees = set([])
153c5f01b2fSopenharmony_ci        self.tree_lock = Lock()
154c5f01b2fSopenharmony_ci        self.scan(root_dir)
155c5f01b2fSopenharmony_ci        self.created_bucket = DirectoryEventBucket(self.scan)
156c5f01b2fSopenharmony_ci        self.observer = Observer()
157c5f01b2fSopenharmony_ci        self.observer.schedule(self, root_dir, recursive=True)
158c5f01b2fSopenharmony_ci        self.observer.start()
159c5f01b2fSopenharmony_ci
160c5f01b2fSopenharmony_ci    def scan(self, base_dir):
161c5f01b2fSopenharmony_ci        scan_dirs = set([base_dir])
162c5f01b2fSopenharmony_ci        # recursively scan base_dir for working trees
163c5f01b2fSopenharmony_ci
164c5f01b2fSopenharmony_ci        while scan_dirs:
165c5f01b2fSopenharmony_ci            scan_dir = os.path.abspath(scan_dirs.pop())
166c5f01b2fSopenharmony_ci            self.scan_tree(scan_dir)
167c5f01b2fSopenharmony_ci            try:
168c5f01b2fSopenharmony_ci                with os.scandir(scan_dir) as dir_it:
169c5f01b2fSopenharmony_ci                    for entry in dir_it:
170c5f01b2fSopenharmony_ci                        if entry.is_dir():
171c5f01b2fSopenharmony_ci                            scan_dirs.add(entry.path)
172c5f01b2fSopenharmony_ci            except FileNotFoundError as e:
173c5f01b2fSopenharmony_ci                logging.debug('path disappeared: %s', e)
174c5f01b2fSopenharmony_ci
175c5f01b2fSopenharmony_ci    def scan_tree(self, scan_dir):
176c5f01b2fSopenharmony_ci        if not is_project_root(scan_dir):
177c5f01b2fSopenharmony_ci            return
178c5f01b2fSopenharmony_ci
179c5f01b2fSopenharmony_ci        # skip source trees in build directories
180c5f01b2fSopenharmony_ci        # this check could be enhanced
181c5f01b2fSopenharmony_ci        if scan_dir.endswith('/_deps/json-src'):
182c5f01b2fSopenharmony_ci            return
183c5f01b2fSopenharmony_ci
184c5f01b2fSopenharmony_ci        tree = WorkTree(self.root_dir, scan_dir)
185c5f01b2fSopenharmony_ci        with self.tree_lock:
186c5f01b2fSopenharmony_ci            if not tree in self.trees:
187c5f01b2fSopenharmony_ci                if tree.name == tree.rel_dir:
188c5f01b2fSopenharmony_ci                    logging.info(f'adding working tree {tree.name}')
189c5f01b2fSopenharmony_ci                else:
190c5f01b2fSopenharmony_ci                    logging.info(f'adding working tree {tree.name} at {tree.rel_dir}')
191c5f01b2fSopenharmony_ci                url = os.path.join('/', tree.rel_dir, HEADER)
192c5f01b2fSopenharmony_ci                logging.info(f'{tree.name}: serving header at {url}')
193c5f01b2fSopenharmony_ci                self.trees.add(tree)
194c5f01b2fSopenharmony_ci
195c5f01b2fSopenharmony_ci    def rescan(self, path=None):
196c5f01b2fSopenharmony_ci        if path is not None:
197c5f01b2fSopenharmony_ci            path = os.path.abspath(path)
198c5f01b2fSopenharmony_ci        trees = set([])
199c5f01b2fSopenharmony_ci        # check if any working trees have been removed
200c5f01b2fSopenharmony_ci        with self.tree_lock:
201c5f01b2fSopenharmony_ci            while self.trees:
202c5f01b2fSopenharmony_ci                tree = self.trees.pop()
203c5f01b2fSopenharmony_ci                if ((path is None
204c5f01b2fSopenharmony_ci                    or os.path.commonpath([path, tree.tree_dir]) == tree.tree_dir)
205c5f01b2fSopenharmony_ci                    and not is_project_root(tree.tree_dir)):
206c5f01b2fSopenharmony_ci                    if tree.name == tree.rel_dir:
207c5f01b2fSopenharmony_ci                        logging.info(f'removing working tree {tree.name}')
208c5f01b2fSopenharmony_ci                    else:
209c5f01b2fSopenharmony_ci                        logging.info(f'removing working tree {tree.name} at {tree.rel_dir}')
210c5f01b2fSopenharmony_ci                else:
211c5f01b2fSopenharmony_ci                    trees.add(tree)
212c5f01b2fSopenharmony_ci            self.trees = trees
213c5f01b2fSopenharmony_ci
214c5f01b2fSopenharmony_ci    def find(self, path):
215c5f01b2fSopenharmony_ci        # find working tree for a given header file path
216c5f01b2fSopenharmony_ci        path = os.path.abspath(path)
217c5f01b2fSopenharmony_ci        with self.tree_lock:
218c5f01b2fSopenharmony_ci            for tree in self.trees:
219c5f01b2fSopenharmony_ci                if path == tree.header:
220c5f01b2fSopenharmony_ci                    return tree
221c5f01b2fSopenharmony_ci        return None
222c5f01b2fSopenharmony_ci
223c5f01b2fSopenharmony_ci    def on_any_event(self, event):
224c5f01b2fSopenharmony_ci        logging.debug('%s (is_dir=%s): %s', event.event_type,
225c5f01b2fSopenharmony_ci                      event.is_directory, event.src_path)
226c5f01b2fSopenharmony_ci        path = os.path.abspath(event.src_path)
227c5f01b2fSopenharmony_ci        if event.is_directory:
228c5f01b2fSopenharmony_ci            if event.event_type == 'created':
229c5f01b2fSopenharmony_ci                # check for new working trees
230c5f01b2fSopenharmony_ci                self.created_bucket.add_dir(path)
231c5f01b2fSopenharmony_ci            elif event.event_type == 'deleted':
232c5f01b2fSopenharmony_ci                # check for deleted working trees
233c5f01b2fSopenharmony_ci                self.rescan(path)
234c5f01b2fSopenharmony_ci        elif event.event_type == 'closed':
235c5f01b2fSopenharmony_ci            with self.tree_lock:
236c5f01b2fSopenharmony_ci                for tree in self.trees:
237c5f01b2fSopenharmony_ci                    tree.update_dirty(path)
238c5f01b2fSopenharmony_ci
239c5f01b2fSopenharmony_ci    def stop(self):
240c5f01b2fSopenharmony_ci        self.observer.stop()
241c5f01b2fSopenharmony_ci        self.observer.join()
242c5f01b2fSopenharmony_ci
243c5f01b2fSopenharmony_ciclass HeaderRequestHandler(SimpleHTTPRequestHandler): # lgtm[py/missing-call-to-init]
244c5f01b2fSopenharmony_ci    def __init__(self, request, client_address, server):
245c5f01b2fSopenharmony_ci        """."""
246c5f01b2fSopenharmony_ci        self.worktrees = server.worktrees
247c5f01b2fSopenharmony_ci        self.worktree = None
248c5f01b2fSopenharmony_ci        try:
249c5f01b2fSopenharmony_ci            super().__init__(request, client_address, server,
250c5f01b2fSopenharmony_ci                             directory=server.worktrees.root_dir)
251c5f01b2fSopenharmony_ci        except ConnectionResetError:
252c5f01b2fSopenharmony_ci            logging.debug('connection reset by peer')
253c5f01b2fSopenharmony_ci
254c5f01b2fSopenharmony_ci    def translate_path(self, path):
255c5f01b2fSopenharmony_ci        path = os.path.abspath(super().translate_path(path))
256c5f01b2fSopenharmony_ci
257c5f01b2fSopenharmony_ci        # add single_include/nlohmann into path, if needed
258c5f01b2fSopenharmony_ci        header = os.path.join('/', HEADER)
259c5f01b2fSopenharmony_ci        header_path = os.path.join('/', SINGLE_INCLUDE, HEADER)
260c5f01b2fSopenharmony_ci        if (path.endswith(header)
261c5f01b2fSopenharmony_ci            and not path.endswith(header_path)):
262c5f01b2fSopenharmony_ci            path = os.path.join(os.path.dirname(path), SINGLE_INCLUDE, HEADER)
263c5f01b2fSopenharmony_ci
264c5f01b2fSopenharmony_ci        return path
265c5f01b2fSopenharmony_ci
266c5f01b2fSopenharmony_ci    def send_head(self):
267c5f01b2fSopenharmony_ci        # check if the translated path matches a working tree
268c5f01b2fSopenharmony_ci        # and fullfill the request; otherwise, send 404
269c5f01b2fSopenharmony_ci        path = self.translate_path(self.path)
270c5f01b2fSopenharmony_ci        self.worktree = self.worktrees.find(path)
271c5f01b2fSopenharmony_ci        if self.worktree is not None:
272c5f01b2fSopenharmony_ci            self.worktree.amalgamate_header()
273c5f01b2fSopenharmony_ci            logging.info(f'{self.worktree.name}; serving header (build count {self.worktree.build_count})')
274c5f01b2fSopenharmony_ci            return super().send_head()
275c5f01b2fSopenharmony_ci        logging.info(f'invalid request path: {self.path}')
276c5f01b2fSopenharmony_ci        super().send_error(HTTPStatus.NOT_FOUND, 'Not Found')
277c5f01b2fSopenharmony_ci        return None
278c5f01b2fSopenharmony_ci
279c5f01b2fSopenharmony_ci    def send_header(self, keyword, value):
280c5f01b2fSopenharmony_ci        # intercept Content-Length header; sent in copyfile later
281c5f01b2fSopenharmony_ci        if keyword == 'Content-Length':
282c5f01b2fSopenharmony_ci            return
283c5f01b2fSopenharmony_ci        super().send_header(keyword, value)
284c5f01b2fSopenharmony_ci
285c5f01b2fSopenharmony_ci    def end_headers (self):
286c5f01b2fSopenharmony_ci        # intercept; called in copyfile() or indirectly
287c5f01b2fSopenharmony_ci        # by send_head via super().send_error()
288c5f01b2fSopenharmony_ci        pass
289c5f01b2fSopenharmony_ci
290c5f01b2fSopenharmony_ci    def copyfile(self, source, outputfile):
291c5f01b2fSopenharmony_ci        injected = False
292c5f01b2fSopenharmony_ci        content = BytesIO()
293c5f01b2fSopenharmony_ci        length = 0
294c5f01b2fSopenharmony_ci        # inject build count and time into served header
295c5f01b2fSopenharmony_ci        for line in source:
296c5f01b2fSopenharmony_ci            line = line.decode('utf-8')
297c5f01b2fSopenharmony_ci            if not injected and JSON_VERSION_RE.match(line):
298c5f01b2fSopenharmony_ci                length += content.write(bytes('#define JSON_BUILD_COUNT '\
299c5f01b2fSopenharmony_ci                                              f'{self.worktree.build_count}\n', 'utf-8'))
300c5f01b2fSopenharmony_ci                length += content.write(bytes('#define JSON_BUILD_TIME '\
301c5f01b2fSopenharmony_ci                                              f'"{self.worktree.build_time}"\n\n', 'utf-8'))
302c5f01b2fSopenharmony_ci                injected = True
303c5f01b2fSopenharmony_ci            length += content.write(bytes(line, 'utf-8'))
304c5f01b2fSopenharmony_ci
305c5f01b2fSopenharmony_ci        # set content length
306c5f01b2fSopenharmony_ci        super().send_header('Content-Length', length)
307c5f01b2fSopenharmony_ci        # CORS header
308c5f01b2fSopenharmony_ci        self.send_header('Access-Control-Allow-Origin', '*')
309c5f01b2fSopenharmony_ci        # prevent caching
310c5f01b2fSopenharmony_ci        self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
311c5f01b2fSopenharmony_ci        self.send_header('Pragma', 'no-cache')
312c5f01b2fSopenharmony_ci        self.send_header('Expires', '0')
313c5f01b2fSopenharmony_ci        super().end_headers()
314c5f01b2fSopenharmony_ci
315c5f01b2fSopenharmony_ci        # send the header
316c5f01b2fSopenharmony_ci        content.seek(0)
317c5f01b2fSopenharmony_ci        shutil.copyfileobj(content, outputfile)
318c5f01b2fSopenharmony_ci
319c5f01b2fSopenharmony_ci    def log_message(self, format, *args):
320c5f01b2fSopenharmony_ci        pass
321c5f01b2fSopenharmony_ci
322c5f01b2fSopenharmony_ciclass DualStackServer(ThreadingHTTPServer):
323c5f01b2fSopenharmony_ci    def __init__(self, addr, worktrees):
324c5f01b2fSopenharmony_ci        """."""
325c5f01b2fSopenharmony_ci        self.worktrees = worktrees
326c5f01b2fSopenharmony_ci        super().__init__(addr, HeaderRequestHandler)
327c5f01b2fSopenharmony_ci
328c5f01b2fSopenharmony_ci    def server_bind(self):
329c5f01b2fSopenharmony_ci        # suppress exception when protocol is IPv4
330c5f01b2fSopenharmony_ci        with contextlib.suppress(Exception):
331c5f01b2fSopenharmony_ci            self.socket.setsockopt(
332c5f01b2fSopenharmony_ci                socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
333c5f01b2fSopenharmony_ci        return super().server_bind()
334c5f01b2fSopenharmony_ci
335c5f01b2fSopenharmony_ciif __name__ == '__main__':
336c5f01b2fSopenharmony_ci    import argparse
337c5f01b2fSopenharmony_ci    import ssl
338c5f01b2fSopenharmony_ci    import socket
339c5f01b2fSopenharmony_ci    import yaml
340c5f01b2fSopenharmony_ci
341c5f01b2fSopenharmony_ci    # exit code
342c5f01b2fSopenharmony_ci    ec = 0
343c5f01b2fSopenharmony_ci
344c5f01b2fSopenharmony_ci    # setup logging
345c5f01b2fSopenharmony_ci    logging.basicConfig(format='[%(asctime)s] %(levelname)s: %(message)s',
346c5f01b2fSopenharmony_ci                        datefmt=DATETIME_FORMAT, level=logging.INFO)
347c5f01b2fSopenharmony_ci    log = logging.getLogger()
348c5f01b2fSopenharmony_ci    log.addHandler(ExitHandler(logging.ERROR))
349c5f01b2fSopenharmony_ci
350c5f01b2fSopenharmony_ci    # parse command line arguments
351c5f01b2fSopenharmony_ci    parser = argparse.ArgumentParser()
352c5f01b2fSopenharmony_ci    parser.add_argument('--make', default='make',
353c5f01b2fSopenharmony_ci                        help='the make command (default: make)')
354c5f01b2fSopenharmony_ci    args = parser.parse_args()
355c5f01b2fSopenharmony_ci
356c5f01b2fSopenharmony_ci    # propagate the make command to use for amalgamating headers
357c5f01b2fSopenharmony_ci    WorkTree.make_command = args.make
358c5f01b2fSopenharmony_ci
359c5f01b2fSopenharmony_ci    worktrees = None
360c5f01b2fSopenharmony_ci    try:
361c5f01b2fSopenharmony_ci        # change working directory to project root
362c5f01b2fSopenharmony_ci        os.chdir(os.path.realpath(os.path.join(sys.path[0], '../../')))
363c5f01b2fSopenharmony_ci
364c5f01b2fSopenharmony_ci        if not is_project_root():
365c5f01b2fSopenharmony_ci            log.error('working directory does not look like project root')
366c5f01b2fSopenharmony_ci
367c5f01b2fSopenharmony_ci        # load config
368c5f01b2fSopenharmony_ci        config = {}
369c5f01b2fSopenharmony_ci        config_file = os.path.abspath(CONFIG_FILE)
370c5f01b2fSopenharmony_ci        try:
371c5f01b2fSopenharmony_ci            with open(config_file, 'r') as f:
372c5f01b2fSopenharmony_ci                config = yaml.safe_load(f)
373c5f01b2fSopenharmony_ci        except FileNotFoundError:
374c5f01b2fSopenharmony_ci            log.info(f'cannot find configuration file: {config_file}')
375c5f01b2fSopenharmony_ci            log.info('using default configuration')
376c5f01b2fSopenharmony_ci
377c5f01b2fSopenharmony_ci        # find and monitor working trees
378c5f01b2fSopenharmony_ci        worktrees = WorkTrees(config.get('root', '.'))
379c5f01b2fSopenharmony_ci
380c5f01b2fSopenharmony_ci        # start web server
381c5f01b2fSopenharmony_ci        infos = socket.getaddrinfo(config.get('bind', None), config.get('port', 8443),
382c5f01b2fSopenharmony_ci                                   type=socket.SOCK_STREAM, flags=socket.AI_PASSIVE)
383c5f01b2fSopenharmony_ci        DualStackServer.address_family = infos[0][0]
384c5f01b2fSopenharmony_ci        HeaderRequestHandler.protocol_version = 'HTTP/1.0'
385c5f01b2fSopenharmony_ci        with DualStackServer(infos[0][4], worktrees) as httpd:
386c5f01b2fSopenharmony_ci            scheme = 'HTTP'
387c5f01b2fSopenharmony_ci            https = config.get('https', {})
388c5f01b2fSopenharmony_ci            if https.get('enabled', True):
389c5f01b2fSopenharmony_ci                cert_file = https.get('cert_file', 'localhost.pem')
390c5f01b2fSopenharmony_ci                key_file = https.get('key_file', 'localhost-key.pem')
391c5f01b2fSopenharmony_ci                ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
392c5f01b2fSopenharmony_ci                ssl_ctx.minimum_version = ssl.TLSVersion.TLSv1_2
393c5f01b2fSopenharmony_ci                ssl_ctx.maximum_version = ssl.TLSVersion.MAXIMUM_SUPPORTED
394c5f01b2fSopenharmony_ci                ssl_ctx.load_cert_chain(cert_file, key_file)
395c5f01b2fSopenharmony_ci                httpd.socket = ssl_ctx.wrap_socket(httpd.socket, server_side=True)
396c5f01b2fSopenharmony_ci                scheme = 'HTTPS'
397c5f01b2fSopenharmony_ci            host, port = httpd.socket.getsockname()[:2]
398c5f01b2fSopenharmony_ci            log.info(f'serving {scheme} on {host} port {port}')
399c5f01b2fSopenharmony_ci            log.info('press Ctrl+C to exit')
400c5f01b2fSopenharmony_ci            httpd.serve_forever()
401c5f01b2fSopenharmony_ci
402c5f01b2fSopenharmony_ci    except KeyboardInterrupt:
403c5f01b2fSopenharmony_ci        log.info('exiting')
404c5f01b2fSopenharmony_ci    except Exception:
405c5f01b2fSopenharmony_ci        ec = 1
406c5f01b2fSopenharmony_ci        log.exception('an error occurred:')
407c5f01b2fSopenharmony_ci    finally:
408c5f01b2fSopenharmony_ci        if worktrees is not None:
409c5f01b2fSopenharmony_ci            worktrees.stop()
410c5f01b2fSopenharmony_ci            sys.exit(ec)
411