Skip to content

Commit 0fefbca

Browse files
committed
WIP: Integrate ProcessManager methods into ShellExecutor
1 parent 092529d commit 0fefbca

File tree

2 files changed

+161
-32
lines changed

2 files changed

+161
-32
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import shlex
2+
from typing import Dict, List, Tuple, Union
3+
4+
5+
class CommandPreProcessor:
6+
"""
7+
Pre-processes and validates shell commands before execution
8+
"""
9+
10+
def preprocess_command(self, command: List[str]) -> List[str]:
11+
"""
12+
Preprocess the command to handle cases where '|' is attached to a command.
13+
"""
14+
preprocessed_command = []
15+
for token in command:
16+
if token in ["||", "&&", ";"]: # Special shell operators
17+
preprocessed_command.append(token)
18+
elif "|" in token and token != "|":
19+
parts = token.split("|")
20+
preprocessed_command.extend(
21+
[part.strip() for part in parts if part.strip()]
22+
)
23+
preprocessed_command.append("|")
24+
else:
25+
preprocessed_command.append(token)
26+
return preprocessed_command
27+
28+
def clean_command(self, command: List[str]) -> List[str]:
29+
"""
30+
Clean command by trimming whitespace from each part.
31+
Removes empty strings but preserves arguments that are meant to be spaces.
32+
33+
Args:
34+
command (List[str]): Original command and its arguments
35+
36+
Returns:
37+
List[str]: Cleaned command
38+
"""
39+
return [arg for arg in command if arg] # Remove empty strings
40+
41+
def create_shell_command(self, command: List[str]) -> str:
42+
"""
43+
Create a shell command string from a list of arguments.
44+
Handles wildcards and arguments properly.
45+
"""
46+
if not command:
47+
return ""
48+
49+
escaped_args = []
50+
for arg in command:
51+
if arg.isspace():
52+
# Wrap space-only arguments in single quotes
53+
escaped_args.append(f"'{arg}'")
54+
else:
55+
# Properly escape all arguments including those with wildcards
56+
escaped_args.append(shlex.quote(arg.strip()))
57+
58+
return " ".join(escaped_args)
59+
60+
def split_pipe_commands(self, command: List[str]) -> List[List[str]]:
61+
"""
62+
Split commands by pipe operator into separate commands.
63+
64+
Args:
65+
command (List[str]): Command and its arguments with pipe operators
66+
67+
Returns:
68+
List[List[str]]: List of commands split by pipe operator
69+
"""
70+
commands: List[List[str]] = []
71+
current_command: List[str] = []
72+
73+
for arg in command:
74+
if arg.strip() == "|":
75+
if current_command:
76+
commands.append(current_command)
77+
current_command = []
78+
else:
79+
current_command.append(arg)
80+
81+
if current_command:
82+
commands.append(current_command)
83+
84+
return commands
85+
86+
def parse_command(
87+
self, command: List[str]
88+
) -> Tuple[List[str], Dict[str, Union[None, str, bool]]]:
89+
"""
90+
Parse command and extract redirections.
91+
"""
92+
cmd = []
93+
redirects: Dict[str, Union[None, str, bool]] = {
94+
"stdin": None,
95+
"stdout": None,
96+
"stdout_append": False,
97+
}
98+
99+
i = 0
100+
while i < len(command):
101+
token = command[i]
102+
103+
# Shell operators check
104+
if token in ["|", ";", "&&", "||"]:
105+
raise ValueError(f"Unexpected shell operator: {token}")
106+
107+
# Output redirection
108+
if token in [">", ">>"]:
109+
if i + 1 >= len(command):
110+
raise ValueError("Missing path for output redirection")
111+
if i + 1 < len(command) and command[i + 1] in [">", ">>", "<"]:
112+
raise ValueError("Invalid redirection target: operator found")
113+
path = command[i + 1]
114+
redirects["stdout"] = path
115+
redirects["stdout_append"] = token == ">>"
116+
i += 2
117+
continue
118+
119+
# Input redirection
120+
if token == "<":
121+
if i + 1 >= len(command):
122+
raise ValueError("Missing path for input redirection")
123+
path = command[i + 1]
124+
redirects["stdin"] = path
125+
i += 2
126+
continue
127+
128+
cmd.append(token)
129+
i += 1
130+
131+
return cmd, redirects

src/mcp_shell_server/shell_executor.py

Lines changed: 30 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -225,13 +225,11 @@ async def execute(
225225
shell_cmd = self.preprocessor.create_shell_command(cmd)
226226
shell_cmd = f"{shell} -i -c {shlex.quote(shell_cmd)}"
227227

228-
process = await asyncio.create_subprocess_shell(
228+
process = await self.process_manager.create_process(
229229
shell_cmd,
230-
stdin=asyncio.subprocess.PIPE,
231-
stdout=stdout_handle,
232-
stderr=asyncio.subprocess.PIPE,
233-
env={**os.environ, **(envs or {})}, # Merge environment variables
234-
cwd=directory,
230+
directory,
231+
stdout_handle=stdout_handle,
232+
envs=envs
235233
)
236234

237235
try:
@@ -248,36 +246,36 @@ async def communicate_with_timeout():
248246
pass
249247
raise e
250248

251-
if timeout:
252-
stdout, stderr = await asyncio.wait_for(
253-
communicate_with_timeout(), timeout=timeout
249+
try:
250+
stdout, stderr = await self.process_manager.execute_with_timeout(
251+
process,
252+
stdin=stdin,
253+
timeout=timeout
254254
)
255-
else:
256-
stdout, stderr = await communicate_with_timeout()
257255

258-
# Close file handle if using file redirection
259-
if isinstance(stdout_handle, IO):
260-
try:
261-
stdout_handle.close()
262-
except (IOError, OSError) as e:
263-
logging.warning(f"Error closing stdout: {e}")
256+
# Close file handle if using file redirection
257+
if isinstance(stdout_handle, IO):
258+
try:
259+
stdout_handle.close()
260+
except (IOError, OSError) as e:
261+
logging.warning(f"Error closing stdout: {e}")
264262

265-
return {
266-
"error": None,
267-
"stdout": stdout.decode() if stdout else "",
268-
"stderr": stderr.decode() if stderr else "",
269-
"status": process.returncode,
270-
"execution_time": time.time() - start_time,
271-
"directory": directory,
272-
}
263+
return {
264+
"error": None,
265+
"stdout": stdout.decode() if stdout else "",
266+
"stderr": stderr.decode() if stderr else "",
267+
"status": process.returncode,
268+
"execution_time": time.time() - start_time,
269+
"directory": directory,
270+
}
273271

274-
except asyncio.TimeoutError:
275-
# Kill process on timeout
276-
try:
277-
process.kill()
278-
await process.wait()
279-
except ProcessLookupError:
280-
pass
272+
except asyncio.TimeoutError:
273+
# Kill process on timeout
274+
try:
275+
process.kill()
276+
await process.wait()
277+
except ProcessLookupError:
278+
pass
281279

282280
# Clean up file handle
283281
if isinstance(stdout_handle, IO):

0 commit comments

Comments
 (0)