blob: defc84186ffafc935a7cc50b6e7d7a3b49b4d0b0 [file] [log] [blame]
Zhuoyao Zhangbae5f722024-09-20 16:53:59 +00001# Copyright 2024, The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15
16import getpass
17import logging
18import multiprocessing.connection
19import os
Zhuoyao Zhang35bd3d22024-10-01 00:16:49 +000020import pathlib
Zhuoyao Zhangbae5f722024-09-20 16:53:59 +000021import platform
22import time
23
24from atest.metrics import clearcut_client
25from atest.proto import clientanalytics_pb2
26from proto import edit_event_pb2
27from watchdog.events import FileSystemEvent
28from watchdog.events import PatternMatchingEventHandler
29from watchdog.observers import Observer
30
31# Enum of the Clearcut log source defined under
32# /google3/wireless/android/play/playlog/proto/log_source_enum.proto
33LOG_SOURCE = 2524
34
35
36class ClearcutEventHandler(PatternMatchingEventHandler):
37
38 def __init__(
39 self, path: str, cclient: clearcut_client.Clearcut | None = None
40 ):
41
42 super().__init__(patterns=["*"], ignore_directories=True)
43 self.root_monitoring_path = path
44 self.cclient = cclient or clearcut_client.Clearcut(LOG_SOURCE)
45
46 self.user_name = getpass.getuser()
47 self.host_name = platform.node()
48 self.source_root = os.environ.get("ANDROID_BUILD_TOP", "")
49
50 def on_moved(self, event: FileSystemEvent):
51 self._log_edit_event(event, edit_event_pb2.EditEvent.MOVE)
52
53 def on_created(self, event: FileSystemEvent):
54 self._log_edit_event(event, edit_event_pb2.EditEvent.CREATE)
55
56 def on_deleted(self, event: FileSystemEvent):
57 self._log_edit_event(event, edit_event_pb2.EditEvent.DELETE)
58
59 def on_modified(self, event: FileSystemEvent):
60 self._log_edit_event(event, edit_event_pb2.EditEvent.MODIFY)
61
62 def flushall(self):
63 logging.info("flushing all pending events.")
64 self.cclient.flush_events()
65
66 def _log_edit_event(
67 self, event: FileSystemEvent, edit_type: edit_event_pb2.EditEvent.EditType
68 ):
Zhuoyao Zhangbae5f722024-09-20 16:53:59 +000069 try:
Zhuoyao Zhang35bd3d22024-10-01 00:16:49 +000070 event_time = time.time()
71
72 if self._is_hidden_file(pathlib.Path(event.src_path)):
73 logging.debug("ignore hidden file: %s.", event.src_path)
74 return
75
76 if not self._is_under_git_project(pathlib.Path(event.src_path)):
77 logging.debug(
78 "ignore file %s which does not belong to a git project",
79 event.src_path,
80 )
81 return
82
83 logging.info("%s: %s", event.event_type, event.src_path)
84
Zhuoyao Zhangbae5f722024-09-20 16:53:59 +000085 event_proto = edit_event_pb2.EditEvent(
86 user_name=self.user_name,
87 host_name=self.host_name,
88 source_root=self.source_root,
89 )
90 event_proto.single_edit_event.CopyFrom(
91 edit_event_pb2.EditEvent.SingleEditEvent(
92 file_path=event.src_path, edit_type=edit_type
93 )
94 )
95 clearcut_log_event = clientanalytics_pb2.LogEvent(
96 event_time_ms=int(event_time * 1000),
97 source_extension=event_proto.SerializeToString(),
98 )
99
100 self.cclient.log(clearcut_log_event)
101 except Exception:
102 logging.exception("Failed to log edit event.")
103
Zhuoyao Zhang35bd3d22024-10-01 00:16:49 +0000104 def _is_hidden_file(self, file_path: pathlib.Path) -> bool:
105 return any(
106 part.startswith(".")
107 for part in file_path.relative_to(self.root_monitoring_path).parts
108 )
109
110 def _is_under_git_project(self, file_path: pathlib.Path) -> bool:
111 root_path = pathlib.Path(self.root_monitoring_path).resolve()
112 return any(
113 root_path.joinpath(dir).joinpath('.git').exists()
114 for dir in file_path.relative_to(root_path).parents
115 )
116
Zhuoyao Zhangbae5f722024-09-20 16:53:59 +0000117
118def start(
119 path: str,
120 cclient: clearcut_client.Clearcut | None = None,
121 pipe_sender: multiprocessing.connection.Connection | None = None,
122):
123 """Method to start the edit monitor.
124
125 This is the entry point to start the edit monitor as a subprocess of
126 the daemon manager.
127
128 params:
129 path: The root path to monitor
130 cclient: The clearcut client to send the edit logs.
131 conn: the sender of the pipe to communicate with the deamon manager.
132 """
133 event_handler = ClearcutEventHandler(path, cclient)
134 observer = Observer()
135
136 logging.info("Starting observer on path %s.", path)
137 observer.schedule(event_handler, path, recursive=True)
138 observer.start()
139 logging.info("Observer started.")
140 if pipe_sender:
141 pipe_sender.send("Observer started.")
142
143 try:
144 while True:
145 time.sleep(1)
146 finally:
147 event_handler.flushall()
148 observer.stop()
149 observer.join()
150 if pipe_sender:
151 pipe_sender.close()