Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion src/extension/src/ProcessHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,27 @@ def is_process_running(self, pid):
# According to "man 2 kill" possible error values are (EINVAL, EPERM, ESRCH) Thus considering this as an error
return False

def is_process_patching_operation(self, pid):
try:
# Patching operation cmdline will look like:
# /usr/bin/python3.10 /var/lib/waagent/Microsoft.CPlat.Core.LinuxPatchExtension-1.6.69/MsftLinuxPatchCore.py <args>
with self.env_layer.file_system.open("/proc/{0}/cmdline".format(str(pid)), mode="rb") as cmdline_file:
cmdline = cmdline_file.read()
Comment thread
michellemcdaniel marked this conversation as resolved.
cmdline = cmdline.replace(b'\x00', b' ').decode("utf-8") # cmdline is null-separated, so replacing nulls with spaces for easier parsing and logging

if Constants.CORE_CODE_FILE_NAME in cmdline:
self.logger.log_verbose("Process is a patching operation. [PID={0}]".format(str(pid)))
return True
else:
self.logger.log_debug("Process is not a patching operation. [PID={0}] [CmdLine={1}]".format(str(pid), cmdline))
return False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar comment to use log_verbose

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please leave as log_debug and include what it actually shows up as instead of our operation. This demonstrates if this actually fixed or changed any outcome at scale.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kjohn-msft
+1 to log what it actually shows up as.

But I think this should be log_verbose than log_debug. log_debug doesn't show up in telemetry so we will have to depend on getting log files from the machine. Having telemetry will be easier to validate in case we see this again

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kjohn-msft @michellemcdaniel re-opening this comment to check on whether this should be log_debug or log_verbose. log_debug won't show up in telemetry

except Exception as error:
self.logger.log_debug("Error checking if process is a patching operation. [PID={0}] [Error={1}]".format(str(pid), repr(error)))
return True # If we cannot determine, assume it is a patching operation to be safe

def kill_process(self, pid):
try:
if self.is_process_running(pid):
if self.is_process_running(pid) and self.is_process_patching_operation(pid):
self.logger.log("Terminating process: [PID={0}]".format(str(pid)))
os.kill(pid, signal.SIGTERM)
except OSError as error:
Expand Down
53 changes: 53 additions & 0 deletions src/extension/tests/Test_ProcessHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
# Requires Python 2.7+

import os
import shutil
import subprocess
import sys
import tempfile
import unittest
from extension.src.Constants import Constants
from extension.src.file_handlers.ExtOutputStatusHandler import ExtOutputStatusHandler
Expand All @@ -39,16 +41,22 @@ def setUp(self):
self.json_file_handler = runtime.json_file_handler
self.env_layer = runtime.env_layer
dir_path = os.path.join(os.path.pardir, "tests", "helpers")
self.test_dir = tempfile.mkdtemp()
self.proc_cmdline_path = os.path.join(self.test_dir, "proc_cmdline")
Comment on lines +44 to +45
self.ext_output_status_handler = ExtOutputStatusHandler(self.logger, self.utility, self.json_file_handler, dir_path)
self.process = subprocess.Popen(["echo", "Hello World!"], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

def tearDown(self):
VirtualTerminal().print_lowlight("\n----------------- tear down test runner -----------------")
shutil.rmtree(self.test_dir)
self.process.kill()

def mock_is_process_running_to_return_true(self, pid):
return True

def mock_is_process_patching_operation_to_return_true(self, pid):
return True

def mock_os_kill_to_raise_exception(self, pid, sig):
raise OSError
Comment thread
michellemcdaniel marked this conversation as resolved.

Expand All @@ -64,6 +72,12 @@ def mock_run_command_output_for_python_not_found(self, cmd, no_output=False, chk
def mock_get_python_cmd(self):
return "python"

def mock_file_system_open_to_return_proc_cmdline(self, path, mode):
return open(self.proc_cmdline_path, mode=mode)

def mock_file_system_open_raises_exception(self, path, mode):
raise FileNotFoundError

Comment on lines +78 to +80
def mock_run_command_to_set_auto_assess_shell_file_permission(self, cmd, no_output=False, chk_err=False):
return 0, "permissions set"

Expand Down Expand Up @@ -133,6 +147,8 @@ def test_kill_process(self):
# setting mocks
is_process_running_backup = ProcessHandler.is_process_running
ProcessHandler.is_process_running = self.mock_is_process_running_to_return_true
is_process_patching_operation_backup = ProcessHandler.is_process_patching_operation
ProcessHandler.is_process_patching_operation = self.mock_is_process_patching_operation_to_return_true
Comment thread
michellemcdaniel marked this conversation as resolved.
os_kill_backup = os.kill
os.kill = self.mock_os_kill_to_raise_exception

Expand All @@ -143,6 +159,7 @@ def test_kill_process(self):

# reseting mocks
ProcessHandler.is_process_running = is_process_running_backup
ProcessHandler.is_process_patching_operation = is_process_patching_operation_backup
os.kill = os_kill_backup

Comment thread
michellemcdaniel marked this conversation as resolved.
def test_get_python_cmd(self):
Expand Down Expand Up @@ -205,6 +222,42 @@ def test_start_daemon(self):
subprocess.Popen = subprocess_popen_backup
process_handler.env_layer.run_command_output = run_command_output_backup
ExtEnvHandler.get_temp_folder = ext_env_handler_get_temp_folder_backup

def test_is_process_patching_operation(self):
# setting mocks
backup_file_system_open = self.env_layer.file_system.open
self.env_layer.file_system.open = self.mock_file_system_open_to_return_proc_cmdline

Comment on lines +227 to +230
# process is patching operation
is_patching_operation_content = b'/usr/bin/python3.10\x00/var/lib/waagent/Microsoft.CPlat.Core.LinuxPatchExtension-1.6.69/MsftLinuxPatchCore.py\x00--seqno\x001234\x00--configfile\x00/var/lib/waagent/Microsoft.CPlat.Core.LinuxPatchExtension-1.6.69/config/1234/config.json\x00--envfile\x00/var/lib/waagent/Microsoft.CPlat.Core.LinuxPatchExtension-1.6.69/env/1234/env.json\x00--outputfile\x00/var/lib/waagent/Microsoft.CPlat.Core.LinuxPatchExtension-1.6.69/output/1234/output.json'
with open(self.proc_cmdline_path, "wb") as f:
f.write(is_patching_operation_content)

process_handler = ProcessHandler(self.logger, self.env_layer, self.ext_output_status_handler)
pid = 1234

self.assertTrue(process_handler.is_process_patching_operation(pid))

# process is not patching operation
is_not_patching_operation_content = b'/usr/bin/python3.10\x00/someother/path/some_other_script.py\x00--somearg\x00somevalue'
with open(self.proc_cmdline_path, "wb") as f:
f.write(is_not_patching_operation_content)

process_handler = ProcessHandler(self.logger, self.env_layer, self.ext_output_status_handler)
pid = 1234

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need to initialize ProcessHandler again or would simply calling is_process_patching_operation() work

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pid is also the same as before, could be reused between both cases


self.assertFalse(process_handler.is_process_patching_operation(pid))

# process cmdline exception
self.env_layer.file_system.open = self.mock_file_system_open_raises_exception

process_handler = ProcessHandler(self.logger, self.env_layer, self.ext_output_status_handler)
pid = 1234

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above


self.assertTrue(process_handler.is_process_patching_operation(pid))

# resetting mocks
self.env_layer.file_system.open = backup_file_system_open

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For one test with multiple scenarios, LPE generally uses a pattern to reduce repetitive lines of code. Refer: test_get_package_manager() in core/tests/Test_EnvLayer.py or test_is_cert_update_supported__with_various_use_cases() in core/src/Test_AptitudePackageManager.py

There's more such examples, sharing these 2.



if __name__ == '__main__':
Expand Down
Loading