Nymbo commited on
Commit
54b5528
·
verified ·
1 Parent(s): 78589bf

Create Modules/Shell_Command.py

Browse files
Files changed (1) hide show
  1. Modules/Shell_Command.py +139 -0
Modules/Shell_Command.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import platform
5
+ import shlex
6
+ import subprocess
7
+ from typing import Annotated
8
+
9
+ import gradio as gr
10
+
11
+ from app import _log_call_end, _log_call_start, _truncate_for_log
12
+ from ._docstrings import autodoc
13
+ from .File_System import _resolve_path, ROOT_DIR, _display_path
14
+ import shutil
15
+
16
+
17
+ def _detect_shell(prefer_powershell: bool = True) -> tuple[list[str], str]:
18
+ """
19
+ Pick an appropriate shell for the host OS.
20
+ - Windows: use PowerShell by default, fall back to cmd.exe.
21
+ - POSIX: use /bin/bash if available, else /bin/sh.
22
+ Returns (shell_cmd_prefix, shell_name) where shell_cmd_prefix is the command list to launch the shell.
23
+ """
24
+ system = platform.system().lower()
25
+ if system == "windows":
26
+ if prefer_powershell:
27
+ pwsh = shutil.which("pwsh")
28
+ candidates = [pwsh, shutil.which("powershell"), shutil.which("powershell.exe")]
29
+ for cand in candidates:
30
+ if cand:
31
+ return [cand, "-NoLogo", "-NoProfile", "-Command"], "powershell"
32
+ # Fallback to cmd
33
+ comspec = os.environ.get("ComSpec", r"C:\\Windows\\System32\\cmd.exe")
34
+ return [comspec, "/C"], "cmd"
35
+ # POSIX
36
+ bash = shutil.which("bash")
37
+ if bash:
38
+ return [bash, "-lc"], "bash"
39
+ sh = os.environ.get("SHELL", "/bin/sh")
40
+ return [sh, "-lc"], "sh"
41
+
42
+
43
+ # Detect shell at import time for docs/UI purposes
44
+ _DETECTED_SHELL_PREFIX, _DETECTED_SHELL_NAME = _detect_shell()
45
+
46
+
47
+ # Clarify path semantics and expose detected shell in summary
48
+ TOOL_SUMMARY = (
49
+ "Execute a shell command within a safe working directory under the tool root ('/'). "
50
+ "Paths must be relative to '/'. "
51
+ "Set workdir to '.' to use the root. "
52
+ "Absolute paths are disabled."
53
+ f"Detected shell: {_DETECTED_SHELL_NAME}."
54
+ )
55
+
56
+
57
+ def _run_command(command: str, cwd: str, timeout: int) -> tuple[str, str, int]:
58
+ shell_prefix, shell_name = _detect_shell()
59
+ full_cmd = shell_prefix + [command]
60
+ try:
61
+ proc = subprocess.run(
62
+ full_cmd,
63
+ cwd=cwd,
64
+ stdout=subprocess.PIPE,
65
+ stderr=subprocess.PIPE,
66
+ text=True,
67
+ encoding="utf-8",
68
+ errors="replace",
69
+ timeout=timeout if timeout and timeout > 0 else None,
70
+ )
71
+ return proc.stdout, proc.stderr, proc.returncode
72
+ except subprocess.TimeoutExpired as exc:
73
+ return exc.stdout or "", (exc.stderr or "") + "\n[timeout]", 124
74
+ except Exception as exc:
75
+ return "", f"Execution failed: {exc}", 1
76
+
77
+
78
+ @autodoc(summary=TOOL_SUMMARY)
79
+ def Shell_Command(
80
+ command: Annotated[str, "Shell command to execute. Accepts multi-part pipelines as a single string."],
81
+ workdir: Annotated[str, "Working directory (relative to root unless UNSAFE_ALLOW_ABS_PATHS=1)."] = ".",
82
+ timeout: Annotated[int, "Timeout in seconds (0 = no timeout, be careful on public hosting)."] = 60,
83
+ ) -> str:
84
+ _log_call_start("Shell_Command", command=command, workdir=workdir, timeout=timeout)
85
+ if not command or not command.strip():
86
+ result = "No command provided."
87
+ _log_call_end("Shell_Command", _truncate_for_log(result))
88
+ return result
89
+
90
+ abs_cwd, err = _resolve_path(workdir)
91
+ if err:
92
+ _log_call_end("Shell_Command", _truncate_for_log(err))
93
+ return err
94
+ if not os.path.exists(abs_cwd):
95
+ result = f"Working directory not found: {abs_cwd}"
96
+ _log_call_end("Shell_Command", _truncate_for_log(result))
97
+ return result
98
+
99
+ # Capture shell used for transparency
100
+ _, shell_name = _detect_shell()
101
+ stdout, stderr, code = _run_command(command, cwd=abs_cwd, timeout=timeout)
102
+ display_cwd = _display_path(abs_cwd)
103
+ header = (
104
+ f"Command: {command}\n"
105
+ f"CWD: {display_cwd}\n"
106
+ f"Root: /\n"
107
+ f"Shell: {shell_name}\n"
108
+ f"Exit code: {code}\n"
109
+ f"--- STDOUT ---\n"
110
+ )
111
+ output = header + (stdout or "<empty>") + "\n--- STDERR ---\n" + (stderr or "<empty>")
112
+ _log_call_end("Shell_Command", _truncate_for_log(f"exit={code} stdout={len(stdout)} stderr={len(stderr)}"))
113
+ return output
114
+
115
+
116
+ def build_interface() -> gr.Interface:
117
+ return gr.Interface(
118
+ fn=Shell_Command,
119
+ inputs=[
120
+ gr.Textbox(label="Command", placeholder="echo hello || dir", lines=2),
121
+ gr.Textbox(label="Workdir", value=".", max_lines=1),
122
+ gr.Slider(minimum=0, maximum=600, step=5, value=60, label="Timeout (seconds)"),
123
+ ],
124
+ outputs=gr.Textbox(label="Output", lines=20),
125
+ title="Shell Command",
126
+ description=(
127
+ "<div style=\"text-align:center; overflow:hidden;\">"
128
+ "Run a shell command under the same safe root as File System. "
129
+ "Absolute paths are disabled, use relative paths. "
130
+ f"Detected shell: {_DETECTED_SHELL_NAME}. "
131
+ "</div>"
132
+ ),
133
+ api_description=TOOL_SUMMARY,
134
+ flagging_mode="never",
135
+ submit_btn="Run",
136
+ )
137
+
138
+
139
+ __all__ = ["Shell_Command", "build_interface"]