Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 148 additions & 1 deletion scripts/daemon.sh
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ set -euo pipefail

# =============================================================================
# wechat-claude-code cross-platform daemon manager
# Supports: macOS (launchd) / Linux (systemd + nohup fallback)
# Supports: macOS (launchd) / Linux (systemd + nohup fallback) / Windows (Git Bash)
# =============================================================================

DATA_DIR="${HOME}/.wechat-claude-code"
Expand Down Expand Up @@ -375,6 +375,139 @@ linux_logs() {
fi
}

# =============================================================================
# Windows (Git Bash / MSYS / Cygwin) functions
# No systemd/launchd here. We launch a fully detached process via PowerShell
# Start-Process so the daemon survives the parent shell exiting (avoids the
# EPIPE crash you get from a plain `nohup ... &` whose stdio is tied to bash).
# =============================================================================

win_pid_file() { echo "${DATA_DIR}/${SERVICE_NAME}.pid"; }

# Convert a possible MSYS path (/c/...) to a Windows path (C:\...) for node.exe.
win_path() {
if command -v cygpath >/dev/null 2>&1; then
cygpath -w "$1"
else
echo "$1"
fi
}

# Liveness check for a NATIVE Windows PID. MSYS `kill -0` only knows processes
# it spawned itself, so it returns false for our PowerShell-launched daemon.
# tasklist sees every process on the machine.
win_pid_alive() {
local pid="$1"
[ -n "$pid" ] || return 1
tasklist //FI "PID eq $pid" //NH 2>/dev/null | grep -q "$pid"
}

win_start() {
local pid_file="$(win_pid_file)"

if [ -f "$pid_file" ]; then
local old_pid=$(cat "$pid_file" 2>/dev/null | tr -d '[:space:]')
if [ -n "$old_pid" ] && win_pid_alive "$old_pid"; then
echo "Already running (PID: $old_pid)"
exit 0
fi
rm -f "$pid_file"
fi

mkdir -p "$DATA_DIR/logs"

local node_bin="$(command -v node || echo node)"
local main_js_win="$(win_path "${PROJECT_DIR}/dist/main.js")"
local out_win="$(win_path "${DATA_DIR}/logs/stdout.log")"
local err_win="$(win_path "${DATA_DIR}/logs/stderr.log")"

echo "Starting wechat-claude-code daemon (Windows detached mode)..."

local wd_win="$(win_path "$PROJECT_DIR")"
local pid_file_win="$(win_path "$pid_file")"

# IMPORTANT: do not capture PowerShell's stdout via $(...). The detached node
# child inherits the pipe handle, so bash never sees EOF and hangs forever.
# Instead PowerShell writes the child PID straight into the pidfile, and we
# fully detach the launcher's stdio from this shell.
local ps_cmd="\$p = Start-Process -FilePath 'node' -ArgumentList @('${main_js_win}','start') -WorkingDirectory '${wd_win}' -WindowStyle Hidden -RedirectStandardOutput '${out_win}' -RedirectStandardError '${err_win}' -PassThru; Set-Content -Path '${pid_file_win}' -Value \$p.Id -NoNewline"
powershell -NoProfile -Command "$ps_cmd" </dev/null >/dev/null 2>&1

# Give PowerShell a moment to spawn the child and write the pidfile.
local tries=0
while [ ! -s "$pid_file" ] && [ $tries -lt 20 ]; do
sleep 0.3
tries=$((tries + 1))
done

local pid="$(cat "$pid_file" 2>/dev/null | tr -d '[:space:]')"
if [ -z "$pid" ]; then
echo "Failed to start (no PID written). Check $err_win"
exit 1
fi

echo "Started (PID: $pid)"
echo "Logs: ${DATA_DIR}/logs/stdout.log"
}

win_stop() {
local pid_file="$(win_pid_file)"

if [ ! -f "$pid_file" ]; then
echo "Not running (no PID file)"
exit 0
fi

local pid=$(cat "$pid_file" 2>/dev/null)
rm -f "$pid_file"

if [ -z "$pid" ]; then
echo "Stopped"
exit 0
fi

# taskkill /T also kills any child processes (e.g. spawned claude CLI).
taskkill //PID "$pid" //T //F >/dev/null 2>&1 && echo "Stopped (PID: $pid)" || echo "Process not running (cleaned up PID file)"
}

win_restart() { win_stop; sleep 1; win_start; }

win_status() {
local pid_file="$(win_pid_file)"

if [ ! -f "$pid_file" ]; then
echo "Not running"
exit 0
fi

local pid=$(cat "$pid_file" 2>/dev/null | tr -d '[:space:]')
if [ -z "$pid" ]; then
echo "Not running (invalid PID file)"
exit 0
fi

if win_pid_alive "$pid"; then
echo "Running (PID: $pid)"
else
echo "Not running (stale PID file)"
fi
}

win_logs() {
local log_dir="${DATA_DIR}/logs"
if [ -d "$log_dir" ]; then
for f in "${log_dir}"/stdout.log "${log_dir}"/stderr.log; do
if [ -f "$f" ]; then
echo "=== $(basename "$f") ==="
tail -50 "$f"
echo ""
fi
done
else
echo "No logs found"
fi
}

# =============================================================================
# Main dispatcher
# =============================================================================
Expand All @@ -383,6 +516,20 @@ main() {
local command="${1:-}"

case "$OS_TYPE" in
MINGW*|MSYS*|CYGWIN*)
case "$command" in
start) win_start ;;
stop) win_stop ;;
restart) win_restart ;;
status) win_status ;;
logs) win_logs ;;
*)
echo "Usage: daemon.sh {start|stop|restart|status|logs}"
echo "Platform: Windows (Git Bash direct mode)"
exit 1
;;
esac
;;
Darwin)
case "$command" in
start) macos_start ;;
Expand Down