CoACT initialize (#292)
This commit is contained in:
@@ -136,6 +136,82 @@ class PythonController:
|
||||
|
||||
logger.error("Failed to execute command.")
|
||||
return None
|
||||
|
||||
def run_python_script(self, script: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Executes a python script on the server.
|
||||
"""
|
||||
payload = json.dumps({"code": script})
|
||||
|
||||
for _ in range(self.retry_times):
|
||||
try:
|
||||
response = requests.post(self.http_server + "/run_python", headers={'Content-Type': 'application/json'},
|
||||
data=payload, timeout=90)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
return {"status": "error", "message": "Failed to execute command.", "output": None, "error": response.json()["error"]}
|
||||
except requests.exceptions.ReadTimeout:
|
||||
break
|
||||
except Exception:
|
||||
logger.error("An error occurred while trying to execute the command: %s", traceback.format_exc())
|
||||
logger.info("Retrying to execute command.")
|
||||
time.sleep(self.retry_interval)
|
||||
|
||||
logger.error("Failed to execute command.")
|
||||
return {"status": "error", "message": "Failed to execute command.", "output": "", "error": "Retry limit reached."}
|
||||
|
||||
def run_bash_script(self, script: str, timeout: int = 30, working_dir: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Executes a bash script on the server.
|
||||
|
||||
:param script: The bash script content (can be multi-line)
|
||||
:param timeout: Execution timeout in seconds (default: 30)
|
||||
:param working_dir: Working directory for script execution (optional)
|
||||
:return: Dictionary with status, output, error, and returncode, or None if failed
|
||||
"""
|
||||
payload = json.dumps({
|
||||
"script": script,
|
||||
"timeout": timeout,
|
||||
"working_dir": working_dir
|
||||
})
|
||||
|
||||
for _ in range(self.retry_times):
|
||||
try:
|
||||
response = requests.post(
|
||||
self.http_server + "/run_bash_script",
|
||||
headers={'Content-Type': 'application/json'},
|
||||
data=payload,
|
||||
timeout=timeout + 100 # Add buffer to HTTP timeout
|
||||
)
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
logger.info("Bash script executed successfully with return code: %d", result.get("returncode", -1))
|
||||
return result
|
||||
else:
|
||||
logger.error("Failed to execute bash script. Status code: %d, response: %s",
|
||||
response.status_code, response.text)
|
||||
logger.info("Retrying to execute bash script.")
|
||||
except requests.exceptions.ReadTimeout:
|
||||
logger.error("Bash script execution timed out")
|
||||
return {
|
||||
"status": "error",
|
||||
"output": "",
|
||||
"error": f"Script execution timed out after {timeout} seconds",
|
||||
"returncode": -1
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("An error occurred while trying to execute the bash script: %s", e)
|
||||
logger.info("Retrying to execute bash script.")
|
||||
time.sleep(self.retry_interval)
|
||||
|
||||
logger.error("Failed to execute bash script after %d retries.", self.retry_times)
|
||||
return {
|
||||
"status": "error",
|
||||
"output": "",
|
||||
"error": f"Failed to execute bash script after {self.retry_times} retries",
|
||||
"returncode": -1
|
||||
}
|
||||
|
||||
def execute_action(self, action: Dict[str, Any]):
|
||||
"""
|
||||
|
||||
@@ -1568,5 +1568,230 @@ def end_recording():
|
||||
return abort(500, description=f"Recording failed. The output file is missing or empty. ffmpeg stderr: {error_output}")
|
||||
|
||||
|
||||
@app.route("/run_python", methods=['POST'])
|
||||
def run_python():
|
||||
data = request.json
|
||||
code = data.get('code', None)
|
||||
|
||||
if not code:
|
||||
return jsonify({'status': 'error', 'message': 'Code not supplied!'}), 400
|
||||
|
||||
# Create a temporary file to save the Python code
|
||||
import tempfile
|
||||
import uuid
|
||||
|
||||
# Generate unique filename
|
||||
temp_filename = f"/tmp/python_exec_{uuid.uuid4().hex}.py"
|
||||
|
||||
try:
|
||||
# Write code to temporary file
|
||||
with open(temp_filename, 'w') as f:
|
||||
f.write(code)
|
||||
|
||||
# Execute the file using subprocess to capture all output
|
||||
result = subprocess.run(
|
||||
['/usr/bin/python3', temp_filename],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
timeout=30 # 30 second timeout
|
||||
)
|
||||
|
||||
# Clean up the temporary file
|
||||
try:
|
||||
os.remove(temp_filename)
|
||||
except:
|
||||
pass # Ignore cleanup errors
|
||||
|
||||
# Prepare response
|
||||
output = result.stdout
|
||||
error_output = result.stderr
|
||||
|
||||
# Combine output and errors if both exist
|
||||
combined_message = output
|
||||
if error_output:
|
||||
combined_message += ('\n' + error_output) if output else error_output
|
||||
|
||||
# Determine status based on return code and errors
|
||||
if result.returncode != 0:
|
||||
status = 'error'
|
||||
if not error_output:
|
||||
# If no stderr but non-zero return code, add a generic error message
|
||||
error_output = f"Process exited with code {result.returncode}"
|
||||
combined_message = combined_message + '\n' + error_output if combined_message else error_output
|
||||
else:
|
||||
status = 'success'
|
||||
|
||||
return jsonify({
|
||||
'status': status,
|
||||
'message': combined_message,
|
||||
'need_more': False, # Not applicable for file execution
|
||||
'output': output, # stdout only
|
||||
'error': error_output, # stderr only
|
||||
'return_code': result.returncode
|
||||
})
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
# Clean up the temporary file on timeout
|
||||
try:
|
||||
os.remove(temp_filename)
|
||||
except:
|
||||
pass
|
||||
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Execution timeout: Code took too long to execute',
|
||||
'error': 'TimeoutExpired',
|
||||
'need_more': False,
|
||||
'output': None,
|
||||
}), 500
|
||||
|
||||
except Exception as e:
|
||||
# Clean up the temporary file on error
|
||||
try:
|
||||
os.remove(temp_filename)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Capture the exception details
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Execution error: {str(e)}',
|
||||
'error': traceback.format_exc(),
|
||||
'need_more': False,
|
||||
'output': None,
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route("/run_bash_script", methods=['POST'])
|
||||
def run_bash_script():
|
||||
data = request.json
|
||||
script = data.get('script', None)
|
||||
timeout = data.get('timeout', 100) # Default timeout of 30 seconds
|
||||
working_dir = data.get('working_dir', None)
|
||||
|
||||
if not script:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'output': 'Script not supplied!',
|
||||
'error': "", # Always empty as requested
|
||||
'returncode': -1
|
||||
}), 400
|
||||
|
||||
# Expand user directory if provided
|
||||
if working_dir:
|
||||
working_dir = os.path.expanduser(working_dir)
|
||||
if not os.path.exists(working_dir):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'output': f'Working directory does not exist: {working_dir}',
|
||||
'error': "", # Always empty as requested
|
||||
'returncode': -1
|
||||
}), 400
|
||||
|
||||
# Create a temporary script file
|
||||
import tempfile
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.sh', delete=False) as tmp_file:
|
||||
if "#!/bin/bash" not in script:
|
||||
script = "#!/bin/bash\n\n" + script
|
||||
tmp_file.write(script)
|
||||
tmp_file_path = tmp_file.name
|
||||
|
||||
try:
|
||||
# Make the script executable
|
||||
os.chmod(tmp_file_path, 0o755)
|
||||
|
||||
# Execute the script
|
||||
if platform_name == "Windows":
|
||||
# On Windows, use Git Bash or WSL if available, otherwise cmd
|
||||
flags = subprocess.CREATE_NO_WINDOW
|
||||
# Try to use bash if available (Git Bash, WSL, etc.)
|
||||
result = subprocess.run(
|
||||
['bash', tmp_file_path],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, # Merge stderr into stdout
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
cwd=working_dir,
|
||||
creationflags=flags,
|
||||
shell=False
|
||||
)
|
||||
else:
|
||||
# On Unix-like systems, use bash directly
|
||||
flags = 0
|
||||
result = subprocess.run(
|
||||
['/bin/bash', tmp_file_path],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, # Merge stderr into stdout
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
cwd=working_dir,
|
||||
creationflags=flags,
|
||||
shell=False
|
||||
)
|
||||
|
||||
# Log the command execution for trajectory recording
|
||||
_append_event("BashScript",
|
||||
{"script": script, "output": result.stdout, "error": "", "returncode": result.returncode},
|
||||
ts=time.time())
|
||||
|
||||
return jsonify({
|
||||
'status': 'success' if result.returncode == 0 else 'error',
|
||||
'output': result.stdout, # Contains both stdout and stderr merged
|
||||
'error': "", # Always empty as requested
|
||||
'returncode': result.returncode
|
||||
})
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'output': f'Script execution timed out after {timeout} seconds',
|
||||
'error': "", # Always empty as requested
|
||||
'returncode': -1
|
||||
}), 500
|
||||
except FileNotFoundError:
|
||||
# Bash not found, try with sh
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['sh', tmp_file_path],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, # Merge stderr into stdout
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
cwd=working_dir,
|
||||
shell=False
|
||||
)
|
||||
|
||||
_append_event("BashScript",
|
||||
{"script": script, "output": result.stdout, "error": "", "returncode": result.returncode},
|
||||
ts=time.time())
|
||||
|
||||
return jsonify({
|
||||
'status': 'success' if result.returncode == 0 else 'error',
|
||||
'output': result.stdout, # Contains both stdout and stderr merged
|
||||
'error': "", # Always empty as requested
|
||||
'returncode': result.returncode,
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'output': f'Failed to execute script: {str(e)}',
|
||||
'error': "", # Always empty as requested
|
||||
'returncode': -1
|
||||
}), 500
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'output': f'Failed to execute script: {str(e)}',
|
||||
'error': "", # Always empty as requested
|
||||
'returncode': -1
|
||||
}), 500
|
||||
finally:
|
||||
# Clean up the temporary file
|
||||
try:
|
||||
os.unlink(tmp_file_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True, host="0.0.0.0")
|
||||
|
||||
Reference in New Issue
Block a user