Skip to content

Commit 5213f86

Browse files
committed
refactor: address PR #184 review feedback
- Security: Add shlex.quote() for systemd service paths and XML escaping for launchd plist paths to handle spaces and special characters - Architecture: Extract shared daemon module (daemon.py) with check_daemon_health(), spawn_daemon_process(), start_daemon_if_needed() to eliminate duplication between start.py and main.py - Fix: Update are_services_running() to accept host/port parameters for accurate health checks - Fix: Add working directory (cwd) parameter to subprocess.Popen for daemon spawning - Fix: Update Dockerfile to use 'cw start --foreground' and healthcheck on management port 9329 - Docs: Update run() docstring to describe auto-start behavior - Refactor: Extract service() function helpers for uninstall and management commands display - Tests: Fix mock paths for daemon module, remove unused variable
1 parent 337ac63 commit 5213f86

File tree

6 files changed

+351
-256
lines changed

6 files changed

+351
-256
lines changed

Dockerfile

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,15 @@ RUN mkdir -p /app/data /app/config /app/.codeweaver && \
101101
# Switch to non-root user
102102
USER codeweaver
103103

104-
# Health check to ensure service is running
104+
# Health check via management server
105105
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
106-
CMD curl -f http://localhost:9328/health/ || exit 1
106+
CMD curl -f http://localhost:9329/health || exit 1
107107

108-
# Expose the MCP server port
109-
EXPOSE 9328
108+
# Expose the MCP HTTP server port (9328) and management server port (9329)
109+
EXPOSE 9328 9329
110110

111-
# Default command: start the CodeWeaver MCP server in stdio mode
112-
# For persistent HTTP service (docker-compose), use: --transport streamable-http
111+
# Default command: start the CodeWeaver daemon in foreground mode
112+
# This runs both management server (9329) and MCP HTTP server (9328)
113+
# For stdio-only mode (MCP clients), use: codeweaver server
113114
ENTRYPOINT ["/entrypoint.sh"]
114-
CMD ["codeweaver", "server"]
115+
CMD ["codeweaver", "start", "--foreground"]

src/codeweaver/cli/commands/init.py

Lines changed: 96 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
from __future__ import annotations
1414

15+
import shlex
1516
import shutil
1617
import sys
1718

@@ -965,15 +966,18 @@ def _get_systemd_unit(cw_cmd: str, working_dir: Path) -> str:
965966
Returns:
966967
Systemd unit file content as a string
967968
"""
969+
# Quote paths to handle spaces and special characters
970+
quoted_cmd = shlex.quote(cw_cmd)
971+
quoted_dir = shlex.quote(str(working_dir))
968972
return f"""[Unit]
969973
Description=CodeWeaver MCP Server - Semantic Code Search
970974
Documentation=https://github.com/knitli/codeweaver
971975
After=network.target
972976
973977
[Service]
974978
Type=simple
975-
ExecStart={cw_cmd} start --foreground
976-
WorkingDirectory={working_dir}
979+
ExecStart={quoted_cmd} start --foreground
980+
WorkingDirectory={quoted_dir}
977981
Restart=on-failure
978982
RestartSec=5
979983
@@ -989,6 +993,17 @@ def _get_systemd_unit(cw_cmd: str, working_dir: Path) -> str:
989993
"""
990994

991995

996+
def _escape_xml(text: str) -> str:
997+
"""Escape special characters for XML content."""
998+
return (
999+
text.replace("&", "&")
1000+
.replace("<", "&lt;")
1001+
.replace(">", "&gt;")
1002+
.replace('"', "&quot;")
1003+
.replace("'", "&apos;")
1004+
)
1005+
1006+
9921007
def _get_launchd_plist(cw_cmd: str, working_dir: Path) -> str:
9931008
"""Generate launchd user agent plist file content.
9941009
@@ -999,6 +1014,12 @@ def _get_launchd_plist(cw_cmd: str, working_dir: Path) -> str:
9991014
Returns:
10001015
Launchd plist file content as a string
10011016
"""
1017+
# Escape paths for XML to handle special characters
1018+
escaped_cmd = _escape_xml(cw_cmd)
1019+
escaped_dir = _escape_xml(str(working_dir))
1020+
escaped_log = _escape_xml(str(Path.home() / "Library" / "Logs" / "codeweaver.log"))
1021+
escaped_err_log = _escape_xml(str(Path.home() / "Library" / "Logs" / "codeweaver.error.log"))
1022+
10021023
return f"""<?xml version="1.0" encoding="UTF-8"?>
10031024
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
10041025
<plist version="1.0">
@@ -1008,13 +1029,13 @@ def _get_launchd_plist(cw_cmd: str, working_dir: Path) -> str:
10081029
10091030
<key>ProgramArguments</key>
10101031
<array>
1011-
<string>{cw_cmd}</string>
1032+
<string>{escaped_cmd}</string>
10121033
<string>start</string>
10131034
<string>--foreground</string>
10141035
</array>
10151036
10161037
<key>WorkingDirectory</key>
1017-
<string>{working_dir}</string>
1038+
<string>{escaped_dir}</string>
10181039
10191040
<key>RunAtLoad</key>
10201041
<true/>
@@ -1026,10 +1047,10 @@ def _get_launchd_plist(cw_cmd: str, working_dir: Path) -> str:
10261047
</dict>
10271048
10281049
<key>StandardOutPath</key>
1029-
<string>{Path.home() / 'Library' / 'Logs' / 'codeweaver.log'}</string>
1050+
<string>{escaped_log}</string>
10301051
10311052
<key>StandardErrorPath</key>
1032-
<string>{Path.home() / 'Library' / 'Logs' / 'codeweaver.error.log'}</string>
1053+
<string>{escaped_err_log}</string>
10331054
10341055
<!-- Environment variables (uncomment and set if needed) -->
10351056
<!--
@@ -1178,6 +1199,69 @@ def _show_windows_instructions(display: StatusDisplay, cw_cmd: str, working_dir:
11781199
display.print_info("\nAlternatively, use Task Scheduler to run CodeWeaver at login.")
11791200

11801201

1202+
def _uninstall_systemd_service(display: StatusDisplay, error_handler: CLIErrorHandler) -> None:
1203+
"""Uninstall the systemd user service on Linux."""
1204+
import subprocess
1205+
1206+
service_file = Path.home() / ".config" / "systemd" / "user" / "codeweaver.service"
1207+
if service_file.exists():
1208+
try:
1209+
subprocess.run(["systemctl", "--user", "stop", "codeweaver.service"], capture_output=True)
1210+
subprocess.run(["systemctl", "--user", "disable", "codeweaver.service"], capture_output=True)
1211+
service_file.unlink()
1212+
subprocess.run(["systemctl", "--user", "daemon-reload"], check=True, capture_output=True)
1213+
display.print_success("Removed systemd service")
1214+
except Exception as e:
1215+
error_handler.handle_error(CodeWeaverError(f"Failed to remove service: {e}"), "Service removal")
1216+
else:
1217+
display.print_warning("Service not installed")
1218+
1219+
1220+
def _uninstall_launchd_service(display: StatusDisplay, error_handler: CLIErrorHandler) -> None:
1221+
"""Uninstall the launchd user agent on macOS."""
1222+
import subprocess
1223+
1224+
plist_file = Path.home() / "Library" / "LaunchAgents" / "li.knit.codeweaver.plist"
1225+
if plist_file.exists():
1226+
try:
1227+
subprocess.run(["launchctl", "unload", str(plist_file)], capture_output=True)
1228+
plist_file.unlink()
1229+
display.print_success("Removed launchd agent")
1230+
except Exception as e:
1231+
error_handler.handle_error(CodeWeaverError(f"Failed to remove agent: {e}"), "Service removal")
1232+
else:
1233+
display.print_warning("Agent not installed")
1234+
1235+
1236+
def _show_systemd_management_commands(display: StatusDisplay) -> None:
1237+
"""Show systemd management commands after successful installation."""
1238+
display.print_section("Management Commands")
1239+
display.print_list(
1240+
[
1241+
"Status: systemctl --user status codeweaver.service",
1242+
"Stop: systemctl --user stop codeweaver.service",
1243+
"Start: systemctl --user start codeweaver.service",
1244+
"Logs: journalctl --user -u codeweaver.service -f",
1245+
"Disable: systemctl --user disable codeweaver.service",
1246+
],
1247+
title="",
1248+
)
1249+
1250+
1251+
def _show_launchd_management_commands(display: StatusDisplay) -> None:
1252+
"""Show launchd management commands after successful installation."""
1253+
display.print_section("Management Commands")
1254+
display.print_list(
1255+
[
1256+
"Status: launchctl list | grep codeweaver",
1257+
"Stop: launchctl unload ~/Library/LaunchAgents/li.knit.codeweaver.plist",
1258+
"Start: launchctl load ~/Library/LaunchAgents/li.knit.codeweaver.plist",
1259+
"Logs: tail -f ~/Library/Logs/codeweaver.log",
1260+
],
1261+
title="",
1262+
)
1263+
1264+
11811265
@app.command
11821266
def service(
11831267
*,
@@ -1239,33 +1323,9 @@ def service(
12391323
# Handle uninstallation
12401324
display.print_section("Uninstalling Service")
12411325
if platform == "linux":
1242-
import subprocess
1243-
1244-
service_file = Path.home() / ".config" / "systemd" / "user" / "codeweaver.service"
1245-
if service_file.exists():
1246-
try:
1247-
subprocess.run(["systemctl", "--user", "stop", "codeweaver.service"], capture_output=True)
1248-
subprocess.run(["systemctl", "--user", "disable", "codeweaver.service"], capture_output=True)
1249-
service_file.unlink()
1250-
subprocess.run(["systemctl", "--user", "daemon-reload"], check=True, capture_output=True)
1251-
display.print_success("Removed systemd service")
1252-
except Exception as e:
1253-
error_handler.handle_error(CodeWeaverError(f"Failed to remove service: {e}"), "Service removal")
1254-
else:
1255-
display.print_warning("Service not installed")
1326+
_uninstall_systemd_service(display, error_handler)
12561327
elif platform == "darwin":
1257-
import subprocess
1258-
1259-
plist_file = Path.home() / "Library" / "LaunchAgents" / "li.knit.codeweaver.plist"
1260-
if plist_file.exists():
1261-
try:
1262-
subprocess.run(["launchctl", "unload", str(plist_file)], capture_output=True)
1263-
plist_file.unlink()
1264-
display.print_success("Removed launchd agent")
1265-
except Exception as e:
1266-
error_handler.handle_error(CodeWeaverError(f"Failed to remove agent: {e}"), "Service removal")
1267-
else:
1268-
display.print_warning("Agent not installed")
1328+
_uninstall_launchd_service(display, error_handler)
12691329
elif platform == "win32":
12701330
display.print_info("To remove Windows service:")
12711331
display.print_info(" nssm remove CodeWeaver confirm")
@@ -1275,32 +1335,11 @@ def service(
12751335
display.print_section("Installing Service")
12761336

12771337
if platform == "linux":
1278-
success = _install_systemd_service(display, cw_cmd, project_path, enable)
1279-
if success:
1280-
display.print_section("Management Commands")
1281-
display.print_list(
1282-
[
1283-
"Status: systemctl --user status codeweaver.service",
1284-
"Stop: systemctl --user stop codeweaver.service",
1285-
"Start: systemctl --user start codeweaver.service",
1286-
"Logs: journalctl --user -u codeweaver.service -f",
1287-
"Disable: systemctl --user disable codeweaver.service",
1288-
],
1289-
title="",
1290-
)
1338+
if _install_systemd_service(display, cw_cmd, project_path, enable):
1339+
_show_systemd_management_commands(display)
12911340
elif platform == "darwin":
1292-
success = _install_launchd_service(display, cw_cmd, project_path, enable)
1293-
if success:
1294-
display.print_section("Management Commands")
1295-
display.print_list(
1296-
[
1297-
"Status: launchctl list | grep codeweaver",
1298-
"Stop: launchctl unload ~/Library/LaunchAgents/li.knit.codeweaver.plist",
1299-
"Start: launchctl load ~/Library/LaunchAgents/li.knit.codeweaver.plist",
1300-
"Logs: tail -f ~/Library/Logs/codeweaver.log",
1301-
],
1302-
title="",
1303-
)
1341+
if _install_launchd_service(display, cw_cmd, project_path, enable):
1342+
_show_launchd_management_commands(display)
13041343
elif platform == "win32":
13051344
_show_windows_instructions(display, cw_cmd, project_path)
13061345
else:

0 commit comments

Comments
 (0)