Skip to content

Commit cc83566

Browse files
committed
feat(fsm): add support to announce Windows processes.
This commit adds cross-platform process announcement to Instana Host Agents. The implementation gracefully handles platform differences, ensuring consistent process information on both Unix and Windows environments: - Created a new `_get_cmdline()` function to return the command line of the current monitored process independently of the running platform. - Created the `_get_cmdline_windows()` function to return the command line on Windows machines. - Created `_get_cmdline_unix()` that returns the command line in Unix machines. It decides how to collect the information by running either the ` _get_cmdline_linux_proc()` or the `_get_cmdline_unix_ps()`. - Refactored the `_setup_socket_connection()` function. Signed-off-by: Paulo Vital <[email protected]>
1 parent b787f70 commit cc83566

File tree

1 file changed

+97
-44
lines changed

1 file changed

+97
-44
lines changed

src/instana/fsm.py

Lines changed: 97 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@
88
import subprocess
99
import sys
1010
import threading
11-
from typing import TYPE_CHECKING, Any, Callable
11+
from typing import TYPE_CHECKING, Any, Callable, List
1212

1313
from fysom import Fysom
1414

1515
from instana.log import logger
1616
from instana.util import get_default_gateway
1717
from instana.util.process_discovery import Discovery
18+
from instana.util.runtime import is_windows
1819
from instana.version import VERSION
1920

2021
if TYPE_CHECKING:
@@ -103,48 +104,16 @@ def lookup_agent_host(self, e: Any) -> bool:
103104
return False
104105

105106
def announce_sensor(self, e: Any) -> bool:
107+
pid: int = os.getpid()
106108
logger.debug(
107-
f"Attempting to make an announcement to the agent on {self.agent.options.agent_host}:{self.agent.options.agent_port}"
109+
f"Attempting to announce PID {pid} to the agent on {self.agent.options.agent_host}:{self.agent.options.agent_port}"
108110
)
109-
pid = os.getpid()
110111

111-
try:
112-
if os.path.isfile("/proc/self/cmdline"):
113-
with open("/proc/self/cmdline") as cmd:
114-
cmdinfo = cmd.read()
115-
cmdline = cmdinfo.split("\x00")
116-
else:
117-
# Python doesn't provide a reliable method to determine what
118-
# the OS process command line may be. Here we are forced to
119-
# rely on ps rather than adding a dependency on something like
120-
# psutil which requires dev packages, gcc etc...
121-
proc = subprocess.Popen(
122-
["ps", "-p", str(pid), "-o", "command"], stdout=subprocess.PIPE
123-
)
124-
(out, _) = proc.communicate()
125-
parts = out.split(b"\n")
126-
cmdline = [parts[1].decode("utf-8")]
127-
except Exception:
128-
cmdline = sys.argv
129-
logger.debug("announce_sensor", exc_info=True)
112+
cmdline = self._get_cmdline(pid)
130113

131114
d = Discovery(pid=self.__get_real_pid(), name=cmdline[0], args=cmdline[1:])
132115

133-
# If we're on a system with a procfs
134-
if os.path.exists("/proc/"):
135-
try:
136-
# In CentOS 7, some odd things can happen such as:
137-
# PermissionError: [Errno 13] Permission denied: '/proc/6/fd/8'
138-
# Use a try/except as a safety
139-
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
140-
sock.connect(
141-
(self.agent.options.agent_host, self.agent.options.agent_port)
142-
)
143-
path = f"/proc/{pid}/fd/{sock.fileno()}"
144-
d.fd = sock.fileno()
145-
d.inode = os.readlink(path)
146-
except: # noqa: E722
147-
logger.debug("Error generating file descriptor: ", exc_info=True)
116+
self._setup_socket_connection(d, pid)
148117

149118
payload = self.agent.announce(d)
150119

@@ -189,28 +158,112 @@ def on_good2go(self, _: Any) -> None:
189158
def __get_real_pid(self) -> int:
190159
"""
191160
Attempts to determine the true process ID by querying the
192-
/proc/<pid>/sched file. This works on systems with a proc filesystem.
193-
Otherwise default to os default.
161+
/proc/<pid>/sched file on Linux systems or using the OS default PID.
162+
For Windows, we use the standard OS PID as there's no equivalent concept
163+
of container PIDs vs host PIDs.
194164
"""
195165
pid = None
196166

167+
# For Linux systems with procfs
197168
if os.path.exists("/proc/"):
198169
sched_file = f"/proc/{os.getpid()}/sched"
199170

200171
if os.path.isfile(sched_file):
201172
try:
202-
file = open(sched_file)
203-
line = file.readline()
204-
g = re.search(r"\((\d+),", line)
205-
if g and len(g.groups()) == 1:
206-
pid = int(g.groups()[0])
173+
with open(sched_file) as file:
174+
line = file.readline()
175+
g = re.search(r"\((\d+),", line)
176+
if g and len(g.groups()) == 1:
177+
pid = int(g.groups()[0])
207178
except Exception:
208179
logger.debug("parsing sched file failed", exc_info=True)
209180

181+
# For Windows or if Linux method failed
210182
if pid is None:
211183
pid = os.getpid()
212184

213185
return pid
214186

187+
def _get_cmdline_windows(self) -> List[str]:
188+
"""
189+
Get command line using Windows API
190+
"""
191+
import ctypes
192+
from ctypes import wintypes
193+
194+
GetCommandLineW = ctypes.windll.kernel32.GetCommandLineW
195+
GetCommandLineW.argtypes = []
196+
GetCommandLineW.restype = wintypes.LPCWSTR
197+
198+
cmd = GetCommandLineW()
199+
# Simple parsing - this is a basic approach and might need refinement
200+
# for complex command lines with quotes and spaces
201+
return cmd.split()
202+
203+
def _get_cmdline_linux_proc(self) -> List[str]:
204+
"""
205+
Get command line from Linux /proc filesystem
206+
"""
207+
with open("/proc/self/cmdline") as cmd:
208+
cmdinfo = cmd.read()
209+
return cmdinfo.split("\x00")
210+
211+
def _get_cmdline_unix_ps(self, pid: int) -> List[str]:
212+
"""
213+
Get command line using ps command (for Unix-like systems without /proc)
214+
"""
215+
proc = subprocess.Popen(
216+
["ps", "-p", str(pid), "-o", "command"], stdout=subprocess.PIPE
217+
)
218+
(out, _) = proc.communicate()
219+
parts = out.split(b"\n")
220+
return [parts[1].decode("utf-8")]
221+
222+
def _get_cmdline_unix(self, pid: int) -> List[str]:
223+
"""
224+
Get command line using Unix
225+
"""
226+
if os.path.isfile("/proc/self/cmdline"):
227+
return self._get_cmdline_linux_proc()
228+
else:
229+
return self._get_cmdline_unix_ps(pid)
230+
231+
def _get_cmdline(self, pid: int) -> List[str]:
232+
"""
233+
Get command line in a platform-independent way
234+
"""
235+
try:
236+
if is_windows():
237+
return self._get_cmdline_windows()
238+
else:
239+
return self._get_cmdline_unix(pid)
240+
except Exception:
241+
logger.debug("Error getting command line", exc_info=True)
242+
return sys.argv
243+
244+
def _setup_socket_connection(self, discovery: Discovery, pid: int) -> None:
245+
"""
246+
Set up socket connection and populate discovery object with socket details
247+
"""
248+
try:
249+
# In CentOS 7, some odd things can happen such as:
250+
# PermissionError: [Errno 13] Permission denied: '/proc/6/fd/8'
251+
# Use a try/except as a safety
252+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
253+
sock.connect((self.agent.options.agent_host, self.agent.options.agent_port))
254+
discovery.fd = sock.fileno()
255+
256+
# If we're on a system with a procfs (Linux)
257+
if os.path.exists("/proc/"):
258+
try:
259+
path = "/proc/%d/fd/%d" % (pid, sock.fileno())
260+
discovery.inode = os.readlink(path)
261+
except Exception:
262+
logger.debug(
263+
"Error generating file descriptor inode: ", exc_info=True
264+
)
265+
except Exception:
266+
logger.debug("Error creating socket connection: ", exc_info=True)
267+
215268

216269
# Made with Bob

0 commit comments

Comments
 (0)