Loading tools/edit_monitor/Android.bp +16 −0 Original line number Diff line number Diff line Loading @@ -36,6 +36,7 @@ python_library_host { srcs: [ "daemon_manager.py", "edit_monitor.py", "utils.py", ], libs: [ "asuite_cc_client", Loading Loading @@ -74,6 +75,21 @@ python_test_host { }, } python_test_host { name: "edit_monitor_utils_test", main: "utils_test.py", pkg_path: "edit_monitor", srcs: [ "utils_test.py", ], libs: [ "edit_monitor_lib", ], test_options: { unit_test: true, }, } python_test_host { name: "edit_monitor_integration_test", main: "edit_monitor_integration_test.py", Loading tools/edit_monitor/daemon_manager.py +10 −0 Original line number Diff line number Diff line Loading @@ -28,6 +28,7 @@ import time from atest.metrics import clearcut_client from atest.proto import clientanalytics_pb2 from edit_monitor import utils from proto import edit_event_pb2 DEFAULT_PROCESS_TERMINATION_TIMEOUT_SECONDS = 5 Loading Loading @@ -79,6 +80,15 @@ class DaemonManager: def start(self): """Writes the pidfile and starts the daemon proces.""" if not utils.is_feature_enabled( "edit_monitor", self.user_name, "ENABLE_EDIT_MONITOR", "EDIT_MONITOR_ROLLOUT_PERCENTAGE", ): logging.warning("Edit monitor is disabled, exiting...") return if self.block_sign.exists(): logging.warning("Block sign found, exiting...") return Loading tools/edit_monitor/daemon_manager_test.py +13 −0 Original line number Diff line number Diff line Loading @@ -81,6 +81,8 @@ class DaemonManagerTest(unittest.TestCase): # Sets the tempdir under the working dir so any temp files created during # tests will be cleaned. tempfile.tempdir = self.working_dir.name self.patch = mock.patch.dict(os.environ, {'ENABLE_EDIT_MONITOR': 'true'}) self.patch.start() def tearDown(self): # Cleans up any child processes left by the tests. Loading @@ -88,6 +90,7 @@ class DaemonManagerTest(unittest.TestCase): self.working_dir.cleanup() # Restores tempdir. tempfile.tempdir = self.original_tempdir self.patch.stop() super().tearDown() def test_start_success_with_no_existing_instance(self): Loading Loading @@ -129,6 +132,15 @@ class DaemonManagerTest(unittest.TestCase): dm = daemon_manager.DaemonManager(TEST_BINARY_FILE) dm.start() # Verify no daemon process is started. self.assertIsNone(dm.daemon_process) @mock.patch.dict(os.environ, {'ENABLE_EDIT_MONITOR': 'false'}, clear=True) def test_start_return_directly_if_disabled(self): dm = daemon_manager.DaemonManager(TEST_BINARY_FILE) dm.start() # Verify no daemon process is started. self.assertIsNone(dm.daemon_process) Loading @@ -137,6 +149,7 @@ class DaemonManagerTest(unittest.TestCase): '/google/cog/cloud/user/workspace/edit_monitor' ) dm.start() # Verify no daemon process is started. self.assertIsNone(dm.daemon_process) Loading tools/edit_monitor/edit_monitor_integration_test.py +6 −1 Original line number Diff line number Diff line Loading @@ -15,7 +15,6 @@ """Integration tests for Edit Monitor.""" import glob from importlib import resources import logging import os import pathlib Loading @@ -27,6 +26,9 @@ import tempfile import time import unittest from importlib import resources from unittest import mock class EditMonitorIntegrationTest(unittest.TestCase): Loading @@ -46,8 +48,11 @@ class EditMonitorIntegrationTest(unittest.TestCase): ) self.root_monitoring_path.mkdir() self.edit_monitor_binary_path = self._import_executable("edit_monitor") self.patch = mock.patch.dict(os.environ, {'ENABLE_EDIT_MONITOR': 'true'}) self.patch.start() def tearDown(self): self.patch.stop() self.working_dir.cleanup() super().tearDown() Loading tools/edit_monitor/utils.py 0 → 100644 +71 −0 Original line number Diff line number Diff line # Copyright 2024, The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import hashlib import logging import os def is_feature_enabled( feature_name: str, user_name: str, enable_flag: str = None, rollout_flag: str = None, ) -> bool: """Determine whether the given feature is enabled. Whether a given feature is enabled or not depends on two flags: 1) the enable_flag that explicitly enable/disable the feature and 2) the rollout_flag that controls the rollout percentage. Args: feature_name: name of the feature. user_name: system user name. enable_flag: name of the env var that enables/disables the feature explicitly. rollout_flg: name of the env var that controls the rollout percentage, the value stored in the env var should be an int between 0 and 100 string """ if enable_flag: if os.environ.get(enable_flag, "") == "false": logging.info("feature: %s is disabled", feature_name) return False if os.environ.get(enable_flag, "") == "true": logging.info("feature: %s is enabled", feature_name) return True if not rollout_flag: return True hash_object = hashlib.sha256() hash_object.update((user_name + feature_name).encode("utf-8")) hash_number = int(hash_object.hexdigest(), 16) % 100 roll_out_percentage = os.environ.get(rollout_flag, "0") try: percentage = int(roll_out_percentage) if percentage < 0 or percentage > 100: logging.warning( "Rollout percentage: %s out of range, disable the feature.", roll_out_percentage, ) return False return hash_number < percentage except ValueError: logging.warning( "Invalid rollout percentage: %s, disable the feature.", roll_out_percentage, ) return False Loading
tools/edit_monitor/Android.bp +16 −0 Original line number Diff line number Diff line Loading @@ -36,6 +36,7 @@ python_library_host { srcs: [ "daemon_manager.py", "edit_monitor.py", "utils.py", ], libs: [ "asuite_cc_client", Loading Loading @@ -74,6 +75,21 @@ python_test_host { }, } python_test_host { name: "edit_monitor_utils_test", main: "utils_test.py", pkg_path: "edit_monitor", srcs: [ "utils_test.py", ], libs: [ "edit_monitor_lib", ], test_options: { unit_test: true, }, } python_test_host { name: "edit_monitor_integration_test", main: "edit_monitor_integration_test.py", Loading
tools/edit_monitor/daemon_manager.py +10 −0 Original line number Diff line number Diff line Loading @@ -28,6 +28,7 @@ import time from atest.metrics import clearcut_client from atest.proto import clientanalytics_pb2 from edit_monitor import utils from proto import edit_event_pb2 DEFAULT_PROCESS_TERMINATION_TIMEOUT_SECONDS = 5 Loading Loading @@ -79,6 +80,15 @@ class DaemonManager: def start(self): """Writes the pidfile and starts the daemon proces.""" if not utils.is_feature_enabled( "edit_monitor", self.user_name, "ENABLE_EDIT_MONITOR", "EDIT_MONITOR_ROLLOUT_PERCENTAGE", ): logging.warning("Edit monitor is disabled, exiting...") return if self.block_sign.exists(): logging.warning("Block sign found, exiting...") return Loading
tools/edit_monitor/daemon_manager_test.py +13 −0 Original line number Diff line number Diff line Loading @@ -81,6 +81,8 @@ class DaemonManagerTest(unittest.TestCase): # Sets the tempdir under the working dir so any temp files created during # tests will be cleaned. tempfile.tempdir = self.working_dir.name self.patch = mock.patch.dict(os.environ, {'ENABLE_EDIT_MONITOR': 'true'}) self.patch.start() def tearDown(self): # Cleans up any child processes left by the tests. Loading @@ -88,6 +90,7 @@ class DaemonManagerTest(unittest.TestCase): self.working_dir.cleanup() # Restores tempdir. tempfile.tempdir = self.original_tempdir self.patch.stop() super().tearDown() def test_start_success_with_no_existing_instance(self): Loading Loading @@ -129,6 +132,15 @@ class DaemonManagerTest(unittest.TestCase): dm = daemon_manager.DaemonManager(TEST_BINARY_FILE) dm.start() # Verify no daemon process is started. self.assertIsNone(dm.daemon_process) @mock.patch.dict(os.environ, {'ENABLE_EDIT_MONITOR': 'false'}, clear=True) def test_start_return_directly_if_disabled(self): dm = daemon_manager.DaemonManager(TEST_BINARY_FILE) dm.start() # Verify no daemon process is started. self.assertIsNone(dm.daemon_process) Loading @@ -137,6 +149,7 @@ class DaemonManagerTest(unittest.TestCase): '/google/cog/cloud/user/workspace/edit_monitor' ) dm.start() # Verify no daemon process is started. self.assertIsNone(dm.daemon_process) Loading
tools/edit_monitor/edit_monitor_integration_test.py +6 −1 Original line number Diff line number Diff line Loading @@ -15,7 +15,6 @@ """Integration tests for Edit Monitor.""" import glob from importlib import resources import logging import os import pathlib Loading @@ -27,6 +26,9 @@ import tempfile import time import unittest from importlib import resources from unittest import mock class EditMonitorIntegrationTest(unittest.TestCase): Loading @@ -46,8 +48,11 @@ class EditMonitorIntegrationTest(unittest.TestCase): ) self.root_monitoring_path.mkdir() self.edit_monitor_binary_path = self._import_executable("edit_monitor") self.patch = mock.patch.dict(os.environ, {'ENABLE_EDIT_MONITOR': 'true'}) self.patch.start() def tearDown(self): self.patch.stop() self.working_dir.cleanup() super().tearDown() Loading
tools/edit_monitor/utils.py 0 → 100644 +71 −0 Original line number Diff line number Diff line # Copyright 2024, The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import hashlib import logging import os def is_feature_enabled( feature_name: str, user_name: str, enable_flag: str = None, rollout_flag: str = None, ) -> bool: """Determine whether the given feature is enabled. Whether a given feature is enabled or not depends on two flags: 1) the enable_flag that explicitly enable/disable the feature and 2) the rollout_flag that controls the rollout percentage. Args: feature_name: name of the feature. user_name: system user name. enable_flag: name of the env var that enables/disables the feature explicitly. rollout_flg: name of the env var that controls the rollout percentage, the value stored in the env var should be an int between 0 and 100 string """ if enable_flag: if os.environ.get(enable_flag, "") == "false": logging.info("feature: %s is disabled", feature_name) return False if os.environ.get(enable_flag, "") == "true": logging.info("feature: %s is enabled", feature_name) return True if not rollout_flag: return True hash_object = hashlib.sha256() hash_object.update((user_name + feature_name).encode("utf-8")) hash_number = int(hash_object.hexdigest(), 16) % 100 roll_out_percentage = os.environ.get(rollout_flag, "0") try: percentage = int(roll_out_percentage) if percentage < 0 or percentage > 100: logging.warning( "Rollout percentage: %s out of range, disable the feature.", roll_out_percentage, ) return False return hash_number < percentage except ValueError: logging.warning( "Invalid rollout percentage: %s, disable the feature.", roll_out_percentage, ) return False