【人类】重新安装 cursor 的 Remote-SSH,以及 Remote-SSH 的脚本做了什么

本文由博主编写

Cursor Remote-SSH 安装之前的重置/清理

问题:原来安装的是 VSCode 的 Remote-SSH,安装 Cursor 后不可用。或者 Cursor 升级到新版本后 Remote-SSH 不可用。

解决:彻底清理不可用版本的残留信息:

  1. 在远程主机上,查找到 vscode / cursor 在远程主机上安装的 remote-ssh 的服务端进程,全部杀死。
  • ps aux | grep vscode | grep -v grep | awk '{print $2}' | xargs kill -9
  • ps aux | grep cursor | grep -v grep | awk '{print $2}' | xargs kill -9
  1. 本地卸载掉 Cursor ,特别是 Curosr 的个性化配置,在 Mac 下是 /Users/${user}/.cursor 的用户.cursor目录,注意这个目录里保留着已经安装的插件的数据。很多建议说只要卸载 Remote-SSH 即可,实际上会遇到很多问题,用彻底卸载是最快的方式重置。

全新 Cursor 安装

去 Cursor 官网下载最新版本,重新安装,安装后打开一个项目,看下 Curosr 的扩展里 Remore-SSH 确实被清理了。

使用 Cursor 打开一个远程主机

  1. 远程主机在本机配置了 ssh 密钥登陆
  2. 使用 Cursor 新窗口的 Connect via SSH 按钮,打开主机,Cursor 会自动提示 Remote-SSH 扩展不存在,会主动安装。

安装 cursor 的 Remote-SSH 的脚本.

上面的步骤可能会执行一段时间就失败了,失败的原因就比较复杂了,这是实际遇到的一个分析过程:

  • 下面这个脚本是从 Cursor 的 Remote-SSH 安装的时候的日志解析后再改造的。
  • Cursor 的 Remote-SSH 会通过 SSH 给远程主机传递一个base64编码的字符串,这个字符串在远程主机上会被解码后通过bash执行。
  • 我把这个base64字符串解析得到了这个 Bash 脚本,然后再改造了下:把在远程主机上下载 cursor-server 的wget命令替换成了在本机下载,再通过 scp 传输给远程主机。解决远程主机可能下载不了 cursor-server 的问题。
  • 我试了下可以成功,但是又发现 Cursor 的 Remote-SSH 似乎每次启动都会重新删除远程主机的 cursor-server 重新安装,后面发现我的主机可以下载 cursor-server,只是下载的慢,于是把 Remote-SSH 的超时时间配置改大点解决了。这个脚本就失去了意义,但是可以放着以后用查看。Remote-SSH 插件如果是开源的话,可以直接去改造它的源代码,让它最我们的开发环境更友好一些。
#!/bin/bash

# =========================================================
# Cursor Remote Server Installation Script
# Used to run the complete Cursor server installation on a remote Linux server
# This script downloads files locally and uploads them to the remote server
# =========================================================

# ==================== Local Configuration ====================
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
LOCAL_DOWNLOAD_DIR="$SCRIPT_DIR/cursor_downloads" # Local download directory

# ==================== Remote Server Configuration ====================
REMOTE_PORT=22 # SSH port
REMOTE_USER="root" # Remote username
REMOTE_HOST="远程机器ID" # Remote host

# ==================== Target Architecture Configuration ====================
# Remote architecture - determines which version you want to use on the remote server
# Possible values: "x64" or "arm64"
# - x64: For Intel/AMD servers (most common)
# - arm64: For ARM-based servers (e.g., AWS Graviton)
REMOTE_ARCH="x64"

# Remote operating system - usually Linux
# Possible values: "linux"
REMOTE_OS="linux"

# ==================== Script Functions ====================

# Output colored messages
print_message() {
    local color=$1
    local message=$2

    case $color in
        "green") echo -e "\033[0;32m$message\033[0m" ;;
        "red") echo -e "\033[0;31m$message\033[0m" ;;
        "yellow") echo -e "\033[0;33m$message\033[0m" ;;
        "blue") echo -e "\033[0;34m$message\033[0m" ;;
        *) echo "$message" ;;
    esac
}

# Check if a command exists
check_command() {
    if ! command -v $1 &> /dev/null; then
        print_message "red" "Error: Command '$1' not found, please install it first."
        exit 1
    fi
}

# Test SSH connection
test_ssh_connection() {
    print_message "blue" "Testing SSH connection to ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PORT}..."
    
    if ssh -p $REMOTE_PORT -o ConnectTimeout=10 -o BatchMode=yes ${REMOTE_USER}@${REMOTE_HOST} "echo 'SSH connection successful'" 2>/dev/null; then
        print_message "green" "SSH connection test successful!"
        return 0
    else
        print_message "red" "SSH connection failed! Please check:"
        print_message "red" "  1. Network connectivity"
        print_message "red" "  2. SSH key authentication is set up"
        print_message "red" "  3. Remote host and port are correct"
        return 1
    fi
}

# Get Cursor version information
get_cursor_version() {
    if ! command -v cursor &> /dev/null; then
        print_message "red" "Error: 'cursor' command not found. Please ensure Cursor is installed."
        exit 1
    fi

    print_message "blue" "Fetching Cursor version information..."

    # Run cursor --version and capture the output
    local version_info=$(cursor --version)

    # Use regex to extract version, commit hash, and architecture
    CURSOR_VERSION=$(echo "$version_info" | sed -n '1p')
    CURSOR_COMMIT=$(echo "$version_info" | sed -n '2p')
    CURSOR_ARCH=$(echo "$version_info" | sed -n '3p')

    print_message "green" "Retrieved Cursor information:"
    print_message "green" "  Version: $CURSOR_VERSION"
    print_message "green" "  Commit: $CURSOR_COMMIT"
    print_message "green" "  Architecture: $CURSOR_ARCH"
}

# Download Cursor server locally
download_cursor_server() {
    print_message "blue" "Preparing to download Cursor server locally..."

    # Create download directory
    mkdir -p "$LOCAL_DOWNLOAD_DIR"

    # Build download URL
    DOWNLOAD_URL="https://cursor.blob.core.windows.net/remote-releases/${CURSOR_VERSION}-${CURSOR_COMMIT}/vscode-reh-${REMOTE_OS}-${REMOTE_ARCH}.tar.gz"

    # Set download filename
    DOWNLOAD_FILENAME="cursor-server-${CURSOR_VERSION}-${CURSOR_COMMIT}-${REMOTE_OS}-${REMOTE_ARCH}.tar.gz"
    DOWNLOAD_PATH="$LOCAL_DOWNLOAD_DIR/$DOWNLOAD_FILENAME"

    # Function to verify downloaded file
    verify_downloaded_file() {
        print_message "yellow" "Verifying downloaded file integrity..."
        if tar -tzf "$DOWNLOAD_PATH" > /dev/null 2>&1; then
            print_message "green" "File verification successful!"
            return 0
        else
            print_message "red" "File verification failed - file is corrupted!"
            return 1
        fi
    }

    # Check if the file already exists and verify it
    if [ -f "$DOWNLOAD_PATH" ]; then
        print_message "green" "Found existing file at $DOWNLOAD_PATH. Verifying..."
        if verify_downloaded_file; then
            print_message "green" "Existing file is valid. Skipping download."
            return
        else
            print_message "yellow" "Existing file is corrupted. Removing and re-downloading..."
            rm "$DOWNLOAD_PATH"
        fi
    fi

    # Download loop with verification
    local max_attempts=3
    local attempt=1

    while [ $attempt -le $max_attempts ]; do
        print_message "yellow" "Download attempt $attempt/$max_attempts"
        print_message "yellow" "Download URL: $DOWNLOAD_URL"
        print_message "yellow" "Downloading to: $DOWNLOAD_PATH"

        # Download the file
        if curl -L "$DOWNLOAD_URL" -o "$DOWNLOAD_PATH"; then
            print_message "green" "Download completed. Verifying file..."
            
            # Verify the downloaded file
            if verify_downloaded_file; then
                print_message "green" "Cursor server downloaded and verified successfully!"
                return
            else
                print_message "red" "Downloaded file is corrupted. Removing file..."
                rm -f "$DOWNLOAD_PATH"
                
                if [ $attempt -eq $max_attempts ]; then
                    print_message "red" "All download attempts failed! Please check your network connection or try again later."
                    exit 1
                fi
                
                print_message "yellow" "Retrying download..."
                ((attempt++))
                sleep 2
            fi
        else
            print_message "red" "Download attempt $attempt failed!"
            
            if [ $attempt -eq $max_attempts ]; then
                print_message "red" "All download attempts failed!"
                exit 1
            fi
            
            print_message "yellow" "Retrying download..."
            ((attempt++))
            sleep 2
        fi
    done
}

# Generate the remote installation script (without download logic)
generate_remote_script() {
    cat << REMOTE_SCRIPT_EOF
#!/bin/bash

# =========================================================
# Cursor Server Setup Script (Remote Execution)
# This script sets up and starts Cursor server on the remote machine
# =========================================================

TMP_DIR="\${XDG_RUNTIME_DIR:-"/tmp"}"
SERVER_COMMIT="$CURSOR_COMMIT"
SERVER_LINE="production"
SERVER_DATA_DIR="\$HOME/.cursor-server"
SERVER_DIR="\$SERVER_DATA_DIR/bin/$CURSOR_COMMIT"
SERVER_NODE_EXECUTABLE="\$SERVER_DIR/node"
CODE_SERVER_SCRIPT="\$SERVER_DIR/bin/cursor-server"
CODE_SERVER_LOGFILE="\$SERVER_DATA_DIR/.\$SERVER_COMMIT.code.log"
CODE_SERVER_TOKENFILE="\$SERVER_DATA_DIR/.\$SERVER_COMMIT.code.token"
CODE_SERVER_PIDFILE="\$SERVER_DATA_DIR/.\$SERVER_COMMIT.code.pid"
CODE_SERVER_PROCESS_ALL_VERSIONS_GREP_PATTERN="\$SERVER_DATA_DIR/bin/.*/out/server-main.js"
CODE_LISTENING_ON=
MULTIPLEX_SERVER_SCRIPT="\$SERVER_DATA_DIR/bin/multiplex-server/533355160f20e458903e84c5050487347b35ee50c16f010b235ae85190cbdbf0.js"
MULTIPLEX_SERVER_LOGFILE="\$SERVER_DATA_DIR/.\$SERVER_COMMIT.multiplex.log"
MULTIPLEX_SERVER_TOKENFILE="\$SERVER_DATA_DIR/.\$SERVER_COMMIT.multiplex.token"
MULTIPLEX_SERVER_PROCESS_ALL_VERSIONS_GREP_PATTERN="\$SERVER_DATA_DIR/bin/multiplex-server/.*.js"
MULTIPLEX_LISTENING_ON=
# This is a magic variable
export VSCODE_AGENT_FOLDER="\$SERVER_DATA_DIR"

print_install_results_and_exit() {
    echo "e1f4c89ebae9a39e5ef64813: start"
    echo "exitCode==\$1=="
    echo "nodeExecutable==\$SERVER_NODE_EXECUTABLE=="
    echo "multiplexListeningOn==\$MULTIPLEX_LISTENING_ON=="
    echo "multiplexConnectionToken==\$MULTIPLEX_SERVER_CONNECTION_TOKEN=="
    echo "codeListeningOn==\$CODE_LISTENING_ON=="
    echo "errorMessage==\$2=="
    echo "isFatalError==\$3=="
    echo "codeConnectionToken==\$CODE_SERVER_CONNECTION_TOKEN=="
    echo "SSH_AUTH_SOCK==\$SSH_AUTH_SOCK=="
    echo "e1f4c89ebae9a39e5ef64813: end"
    exit \$1
}

get_lockfile() {
    # Storing the lockfile in TMP_DIR to avoid issues with nonlocal file systems
    echo "\$TMP_DIR/cursor-remote-lock.\$(echo \$SERVER_DATA_DIR | shasum -a 256 | cut -d' ' -f1)"
}

unlock() {
    lockfile=\$(get_lockfile)
    echo "Unlocking \$lockfile"
    rm -f "\$lockfile"
    rm -f "\$lockfile.target"
}

lock() {
    lockfile=\$(get_lockfile)
    echo "Locking \$lockfile"
    lockAcquired=0
    for i in {1..30}; do
        touch "\$lockfile.target"
        ln "\$lockfile.target" "\$lockfile"
        if [ \$? -eq 0 ]; then
            lockAcquired=1
            break
        fi
        echo "Install in progress, sleeping for a bit..."
        sleep 1
    done
    if [ \$lockAcquired -eq 0 ]; then
        print_install_results_and_exit 1 "Could not acquire lock after multiple attempts"
    fi
    trap unlock EXIT
}

echo "Creating installation directory structure..."
mkdir -p "\$SERVER_DIR"
if (( \$? > 0 )); then
    print_install_results_and_exit 1 "Error creating server install directory \$SERVER_DIR"
fi

lock

# Check if server script is already installed
if [[ ! -f \$CODE_SERVER_SCRIPT ]]; then
    echo "Extracting uploaded server files..."
    if [[ -f ~/.cursor-server/cursor-server.tar.gz ]]; then
        cd "\$SERVER_DIR"
        echo "Extracting cursor-server.tar.gz to \$SERVER_DIR"
        tar -xzf ~/.cursor-server/cursor-server.tar.gz --strip-components=1
        if (( \$? > 0 )); then
            print_install_results_and_exit 1 "Failed to extract uploaded server files"
        fi
        rm -f ~/.cursor-server/cursor-server.tar.gz
        echo "Server files extracted successfully"
    else
        print_install_results_and_exit 1 "No uploaded server files found at ~/.cursor-server/cursor-server.tar.gz"
    fi
    
    if [[ ! -f \$CODE_SERVER_SCRIPT ]]; then
        print_install_results_and_exit 1 "Failed to extract code server script: \$CODE_SERVER_SCRIPT"
    fi
else
    echo "Server script already installed in \$CODE_SERVER_SCRIPT"
fi

echo "Checking node executable"
if ! \$SERVER_NODE_EXECUTABLE --version; then
    echo "Node executable at \$SERVER_NODE_EXECUTABLE is not working, checking system node"
    SYSTEM_NODE=\$(which node)
    if [[ -n "\$SYSTEM_NODE" ]]; then
        echo "System node version: \$(\$SYSTEM_NODE --version)"
        \$SYSTEM_NODE -e "process.exit(parseInt(process.version.slice(1)) >= 20 ? 0 : 1)"
        if [[ \$? -eq 0 ]]; then
            echo "System node version is 20 or higher, creating symlink"
            ln -sf \$SYSTEM_NODE \$SERVER_NODE_EXECUTABLE
            echo "Created symlink from \$SERVER_NODE_EXECUTABLE to \$SYSTEM_NODE"
        else
            print_install_results_and_exit 1 "The bundled NodeJS failed to run, and system node is too old. Please manually install NodeJS 20 or higher on your remote system" "true"
        fi
    else
        print_install_results_and_exit 1 "The bundled NodeJS failed to run, and no system NodeJS executable was found. Please manually install NodeJS 20 or higher on your remote system" "true"
    fi
fi

# Clean up stale builds
OLD_COMMITS=\$(ls -1 -t "\$SERVER_DATA_DIR/bin" 2>/dev/null | tail -n +6)
for OLD_COMMIT in \$OLD_COMMITS; do
    IS_RUNNING=\`ps ax | grep \$OLD_COMMIT | grep -v grep | wc -l | tr -d '[:space:]'\`
    if [[ \$IS_RUNNING -eq 0 ]]; then
        echo "Cleaning up stale build \$OLD_COMMIT"
        if [[ "\$OLD_COMMIT" != "\$SERVER_COMMIT" ]]; then
            rm -rf "\$SERVER_DATA_DIR/bin/\$OLD_COMMIT" "\$SERVER_DATA_DIR/.\$OLD_COMMIT.*"
        fi
    else
        echo "Build \$OLD_COMMIT is still running, skipping"
    fi
done

# Try to find if server is already running
echo "Checking for running multiplex server: \$MULTIPLEX_SERVER_SCRIPT"
MULTIPLEX_SERVER_RUNNING_PROCESS="\$(ps -o pid,args -A | grep \$MULTIPLEX_SERVER_SCRIPT | grep -v grep)"
echo "Running multiplex server: \$MULTIPLEX_SERVER_RUNNING_PROCESS"

if [[ -z \$MULTIPLEX_SERVER_RUNNING_PROCESS || ! -f \$MULTIPLEX_SERVER_LOGFILE || ! -f \$MULTIPLEX_SERVER_TOKENFILE ]]; then
    if [[ -f \$MULTIPLEX_SERVER_LOGFILE ]]; then
        rm \$MULTIPLEX_SERVER_LOGFILE
    fi
    if [[ -f \$MULTIPLEX_SERVER_TOKENFILE ]]; then
        rm \$MULTIPLEX_SERVER_TOKENFILE
    fi

    touch \$MULTIPLEX_SERVER_TOKENFILE
    chmod 600 \$MULTIPLEX_SERVER_TOKENFILE
    MULTIPLEX_SERVER_CONNECTION_TOKEN="65dd0b7c-f1ec-4d58-9626-7b4477658453"
    echo \$MULTIPLEX_SERVER_CONNECTION_TOKEN > \$MULTIPLEX_SERVER_TOKENFILE

    # This is super counterintuitive, but we UNSET the SSH_AUTH_SOCK before starting the server, because we manually inject it into the extension host later.
    # This is to prevent us from being used across ALL connections by default

    # Backing it up, NOT exporting it
    if [[ -n "\$SSH_AUTH_SOCK" ]]; then
        ORIGINAL_SSH_AUTH_SOCK=\$SSH_AUTH_SOCK
        unset SSH_AUTH_SOCK
    fi

    echo "Creating directory for multiplex server: \$(dirname \$MULTIPLEX_SERVER_SCRIPT)"
    mkdir -p \$(dirname "\$MULTIPLEX_SERVER_SCRIPT")
    echo "Writing multiplex server script to \$MULTIPLEX_SERVER_SCRIPT"
    echo '"use strict";var __assign=this&&this.__assign||function(){return __assign=Object.assign||function(e){for(var r,o=1,t=arguments.length;o<t;o++)for(var n in r=arguments[o])Object.prototype.hasOwnProperty.call(r,n)&&(e[n]=r[n]);return e},__assign.apply(this,arguments)};Object.defineProperty(exports,"__esModule",{value:!0});var net_1=require("net"),child_process_1=require("child_process"),crypto_1=require("crypto"),token=process.argv[2];token||(console.error("Token must be provided as the first argument"),process.exit(1));var inactivityTimer,server=(0,net_1.createServer)({keepAlive:!0},(function(e){console.log("Server received connection"),resetInactivityTimer();var r,o,t=Buffer.alloc(0);e.on("close",(function(){console.log("[remoteServer][".concat(o,"] Socket closed; killing child process")),r&&r.pid&&!r.killed&&((null==r?void 0:r.kill())||console.error("[remoteServer][".concat(o,"] Failed to kill child process")))})),e.on("data",(function(n){var i,s,c;if(t=Buffer.concat([t,n]),void 0===r){var a=t.indexOf(Buffer.from("\\n"));if(-1===a)return;var d=t.subarray(0,a).toString();t=t.subarray(a+1);var l=void 0;try{l=JSON.parse(d)}catch(r){return console.error("Error parsing JSON command:",r),void e.destroy()}if(l.token!==token)return console.error("Invalid token:",l.token),e.write(JSON.stringify({type:"exit",code:401})+"\\n"),void e.end();o=null!==(i=l.id)&&void 0!==i?i:(0,crypto_1.randomUUID)(),console.log("[command][".concat(o,"] Executing command: ").concat(l.command," ").concat((null!==(s=l.args)&&void 0!==s?s:[]).join(" "))),(r=(0,child_process_1.spawn)(l.command,null!==(c=l.args)&&void 0!==c?c:[],{env:__assign(__assign({},process.env),l.env),cwd:l.cwd,shell:!1,stdio:"pipe"})).stdout.on("data",(function(r){var o={type:"stdout",data:r.toString("base64")};e.write(JSON.stringify(o)+"\\n")})),r.stdout.on("end",(function(){e.write(JSON.stringify({type:"stdout-end"})+"\\n")})),r.stderr.on("data",(function(r){var o={type:"stderr",data:r.toString("base64")};e.write(JSON.stringify(o)+"\\n")})),r.stderr.on("end",(function(){e.write(JSON.stringify({type:"stderr-end"})+"\\n")}));var u=!1;r.on("error",(function(r){u?console.log("[command][".concat(o,"] Process exited with error but we already sent an exit message"),r):(u=!0,console.error("[command][".concat(o,"] Process error"),r),e.write(JSON.stringify({type:"exit",code:1})+"\\n"),e.end())})),r.on("exit",(function(r){if(u)console.log("[command][".concat(o,"] Process exited with code ").concat(r," but we already sent an exit message"));else{u=!0,console.log("[command][".concat(o,"] Process exited with code ").concat(r,". Sending exit message"));var t={type:"exit",code:null!==r?r:0};e.write(JSON.stringify(t)+"\\n"),e.end()}}))}var v=t.indexOf(0);if(-1!==v){var f=t.subarray(0,v);try{var g=Buffer.from(f.toString("utf-8"),"base64");r.stdin.write(g)}catch(o){return console.error("Error decoding base64 data:",o),e.destroy(),void r.kill("SIGKILL")}return r.stdin.end(),void(t=Buffer.alloc(0))}var m=4*Math.floor(t.length/4);if(0!==m){var y=t.subarray(0,m);t=t.subarray(m);try{g=Buffer.from(y.toString("utf-8"),"base64"),r.stdin.write(g)}catch(o){return console.error("Error decoding base64 data:",o),r.kill("SIGKILL"),void e.destroy()}}})),e.on("error",(function(e){console.error("[remoteServer][".concat(o,"] Socket error:"),e),r&&r.kill("SIGKILL")})),e.setTimeout(3e5,(function(){console.log("[remoteServer][".concat(o,"] No data received for 5 minutes, shutting down...")),e.destroy()}))}));function resetInactivityTimer(){inactivityTimer&&clearTimeout(inactivityTimer),inactivityTimer=setTimeout((function(){console.error("No connections received for 5 minutes, shutting down..."),process.exit(0)}),3e5)}resetInactivityTimer(),server.listen({host:"127.0.0.1",port:0},(function(){var e=server.address();null===e&&(console.error("Failed to get server address"),process.exit(1)),"string"==typeof e&&(console.error("Invalid address: ".concat(e)),process.exit(1)),console.log("Server listening on ".concat(e.port))})),process.on("SIGINT",(function(){server.close((function(){console.log("Server closed"),process.exit(0)}))}));' > "\$MULTIPLEX_SERVER_SCRIPT"

    echo "Starting multiplex server: \$SERVER_NODE_EXECUTABLE \$MULTIPLEX_SERVER_SCRIPT \$MULTIPLEX_SERVER_CONNECTION_TOKEN"
    \$SERVER_NODE_EXECUTABLE "\$MULTIPLEX_SERVER_SCRIPT" "\$MULTIPLEX_SERVER_CONNECTION_TOKEN"  &> \$MULTIPLEX_SERVER_LOGFILE &

    # Restoring the original SSH_AUTH_SOCK
    if [[ -n "\$ORIGINAL_SSH_AUTH_SOCK" ]]; then
        export SSH_AUTH_SOCK=\$ORIGINAL_SSH_AUTH_SOCK
        unset ORIGINAL_SSH_AUTH_SOCK
    fi
else
    echo "Multiplex server script is already running \$MULTIPLEX_SERVER_SCRIPT. Running processes are \$MULTIPLEX_SERVER_RUNNING_PROCESS"
fi

echo "Reading multiplex server token file \$MULTIPLEX_SERVER_TOKENFILE"

if [[ -f \$MULTIPLEX_SERVER_TOKENFILE ]]; then
    echo "Multiplex server token file found"
    MULTIPLEX_SERVER_CONNECTION_TOKEN="\$(cat \$MULTIPLEX_SERVER_TOKENFILE)"
else
    print_install_results_and_exit 1 "Multiplex server token file not found: \$MULTIPLEX_SERVER_TOKENFILE"
fi

echo "Reading multiplex server log file \$MULTIPLEX_SERVER_LOGFILE"

for i in {1..20}; do
    if [[ -f \$MULTIPLEX_SERVER_LOGFILE ]]; then
        MULTIPLEX_LISTENING_ON="\$(cat \$MULTIPLEX_SERVER_LOGFILE | grep -E 'Server listening on .+' | sed 's/Server listening on //')"
        if [[ -n "\$MULTIPLEX_LISTENING_ON" ]]; then
            break
        fi
    fi
    sleep 0.5
done

if [[ -z "\$MULTIPLEX_LISTENING_ON" ]]; then
    echo "Error multiplex server did not start successfully"
    # Reading the logfile, to help with debugging
    cat \$MULTIPLEX_SERVER_LOGFILE || true
    print_install_results_and_exit 1 "Multiplex server did not start successfully"
fi

echo "Checking for code servers"
if [[ -f \$CODE_SERVER_PIDFILE ]]; then
    CODE_SERVER_PID="\$(cat \$CODE_SERVER_PIDFILE)"
    CODE_SERVER_RUNNING_PROCESS="\$(ps -o pid,args | grep -w \$CODE_SERVER_PID | grep \$CODE_SERVER_SCRIPT)"
else
    CODE_SERVER_RUNNING_PROCESS="\$(ps -o pid,args -A | grep \$CODE_SERVER_SCRIPT | grep -v grep)"
fi

# Making sure the token file and log files exists; otherwise we need to start a new server
if [[ -z \$CODE_SERVER_RUNNING_PROCESS || ! -f \$CODE_SERVER_TOKENFILE || ! -f \$CODE_SERVER_LOGFILE ]]; then
    echo "Code server script is not running"
    if [[ -f \$CODE_SERVER_LOGFILE ]]; then
        rm \$CODE_SERVER_LOGFILE
    fi
    if [[ -f \$CODE_SERVER_TOKENFILE ]]; then
        rm \$CODE_SERVER_TOKENFILE
    fi

    touch \$CODE_SERVER_TOKENFILE
    chmod 600 \$CODE_SERVER_TOKENFILE
    CODE_SERVER_CONNECTION_TOKEN="4aec38c5-b9d8-4aca-9508-b88a51c295a5"
    echo \$CODE_SERVER_CONNECTION_TOKEN > \$CODE_SERVER_TOKENFILE

    # This is super counterintuitive, but we UNSET the SSH_AUTH_SOCK before starting the server, because we manually inject it into the extension host later.
    # This is to prevent us from being used across ALL connections by default

    # Backing it up, NOT exporting it
    if [[ -n "\$SSH_AUTH_SOCK" ]]; then
        ORIGINAL_SSH_AUTH_SOCK=\$SSH_AUTH_SOCK
        unset SSH_AUTH_SOCK
    fi

    echo "Starting code server script \$CODE_SERVER_SCRIPT --start-server --host=127.0.0.1 --port 0  --connection-token-file \$CODE_SERVER_TOKENFILE --telemetry-level off --enable-remote-auto-shutdown --accept-server-license-terms &> \$CODE_SERVER_LOGFILE &"

    \$CODE_SERVER_SCRIPT --start-server --host=127.0.0.1 --port 0  --connection-token-file \$CODE_SERVER_TOKENFILE --telemetry-level off --enable-remote-auto-shutdown --accept-server-license-terms &> \$CODE_SERVER_LOGFILE &
    echo \$! > \$CODE_SERVER_PIDFILE

    # Restoring the original SSH_AUTH_SOCK
    if [[ -n "\$ORIGINAL_SSH_AUTH_SOCK" ]]; then
        export SSH_AUTH_SOCK=\$ORIGINAL_SSH_AUTH_SOCK
        unset ORIGINAL_SSH_AUTH_SOCK
    fi
else
    echo "Code server script is already running \$CODE_SERVER_SCRIPT. Running processes are \$CODE_SERVER_RUNNING_PROCESS"
fi

if [[ -f \$CODE_SERVER_TOKENFILE ]]; then
    CODE_SERVER_CONNECTION_TOKEN="\$(cat \$CODE_SERVER_TOKENFILE)"
else
    print_install_results_and_exit 1 "Code server token file not found: \$CODE_SERVER_TOKENFILE"
fi

for i in {1..20}; do
    if [[ -f \$CODE_SERVER_LOGFILE ]]; then
        CODE_LISTENING_ON="\$(cat \$CODE_SERVER_LOGFILE | grep -E 'Extension host agent listening on .+' | sed 's/Extension host agent listening on //')"
        if [[ -n "\$CODE_LISTENING_ON" ]]; then
            break
        fi
    fi
    sleep 0.5
done

if [[ -z "\$CODE_LISTENING_ON" ]]; then
    echo "Error code server did not start successfully"
    # Reading the logfile, to help with debugging
    cat \$CODE_SERVER_LOGFILE || true
    LOGFILE_CONTENTS=\$(cat \$CODE_SERVER_LOGFILE || echo "")
    print_install_results_and_exit 1 "Code server did not start successfully" "false"
fi

echo "Cursor server installation completed successfully!"
echo "Multiplex server listening on: \$MULTIPLEX_LISTENING_ON"
echo "Code server listening on: \$CODE_LISTENING_ON"
echo "Installation directory: \$SERVER_DIR"

unlock

REMOTE_SCRIPT_EOF
}

# Upload server files to remote host
upload_server_files() {
    print_message "blue" "Uploading Cursor server files to remote host..."

    # Build SSH and SCP command prefixes
    SSH_CMD="ssh -p $REMOTE_PORT ${REMOTE_USER}@${REMOTE_HOST}"
    SCP_CMD="scp -P $REMOTE_PORT"

    # Ensure remote directory exists
    print_message "yellow" "Creating remote directory structure..."
    $SSH_CMD "mkdir -p ~/.cursor-server/"

    if [ $? -ne 0 ]; then
        print_message "red" "Failed to create remote directory!"
        return 1
    fi

    # Upload the file
    print_message "yellow" "Uploading server files to remote host..."
    $SCP_CMD "$DOWNLOAD_PATH" "${REMOTE_USER}@${REMOTE_HOST}:~/.cursor-server/cursor-server.tar.gz"

    if [ $? -ne 0 ]; then
        print_message "red" "Upload failed!"
        return 1
    fi

    print_message "green" "Server files uploaded successfully!"
}

# Execute the installation on remote server
execute_remote_installation() {
    print_message "blue" "Executing Cursor server installation on remote server..."
    
    # Generate the remote script
    REMOTE_SCRIPT_CONTENT=$(generate_remote_script)
    
    # Execute the script on the remote server
    print_message "yellow" "Sending installation script to remote server..."
    ssh -p $REMOTE_PORT ${REMOTE_USER}@${REMOTE_HOST} "bash -s" << EOF
$REMOTE_SCRIPT_CONTENT
EOF
    
    local exit_code=$?
    
    if [ $exit_code -eq 0 ]; then
        print_message "green" "Remote installation completed successfully!"
        print_message "green" "You can now connect to the remote server using Cursor."
    else
        print_message "red" "Remote installation failed with exit code: $exit_code"
        return 1
    fi
}

# ==================== Main Program ====================

print_message "blue" "Cursor Remote Server Installation Script"
print_message "blue" "========================================="

# Check necessary commands
check_command "curl"
check_command "ssh"
check_command "scp"

# Test SSH connection
if ! test_ssh_connection; then
    print_message "red" "Cannot proceed without a working SSH connection."
    exit 1
fi

# Get Cursor version information
get_cursor_version

# Confirm execution
print_message "blue" "This script will install Cursor server on remote host: ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PORT}"
print_message "yellow" "The installation will:"
print_message "yellow" "  1. Download Cursor server files locally (${CURSOR_VERSION}-${CURSOR_COMMIT})"
print_message "yellow" "  2. Upload the files to the remote server"
print_message "yellow" "  3. Extract and set up the server environment"
print_message "yellow" "  4. Start the Cursor server services"
print_message "yellow" ""
print_message "yellow" "Continue with the installation? [y/N]: "
read -r confirmation

if [[ $confirmation =~ ^[Yy]$ ]]; then
    # Download Cursor server locally
    download_cursor_server
    
    # Upload files to remote server
    if upload_server_files; then
        # Execute remote installation
        execute_remote_installation
    else
        print_message "red" "Upload failed, cannot proceed with installation."
        exit 1
    fi
else
    print_message "yellow" "Installation canceled by user."
    exit 0
fi

print_message "green" "Script execution complete!" 

实际上,在没有改造之前,Curosr Remote-SSH 在主机上安装 cursor-server 的逻辑如下:

Cursor Remote-SSH 远程安装 cursor-server 完整逻辑解析

这个 install.sh 脚本是 Cursor Remote-SSH 功能的核心,它在远程主机上自动安装和配置 cursor-server。以下是详细的逻辑解析:

🔧 初始化阶段

  1. 环境配置: 设置关键变量如 SERVER_COMMITSERVER_DATA_DIR、各种日志和token文件路径
  2. 平台检测: 识别操作系统(Linux/Darwin)和CPU架构(x64/arm64)
  3. 兼容性检查: 确保平台和架构被支持,特殊处理Alpine Linux

🔒 安装准备

  1. 目录创建: 在 ~/.cursor-server/bin/{commit_hash}/ 下创建安装目录
  2. 锁机制: 使用文件锁防止并发安装,确保安装过程的原子性
  3. URL构建: 根据平台和架构构建下载URL

📥 服务器下载与安装

  1. 智能下载: 优先使用 wget,fallback 到 curl
  2. 文件验证: 下载 tar.gz 文件并解压到指定目录
  3. 完整性检查: 验证关键文件(如 cursor-server 脚本)是否存在

⚙️ 环境配置

  1. Node.js检查: 验证bundled Node.js可执行性,如果失败则检查系统Node.js
  2. 版本要求: 确保Node.js版本≥20,必要时创建符号链接
  3. 清理工作: 删除旧版本的安装文件,保持系统整洁

🚀 服务启动

  1. Multiplex服务器:

    • 检查是否已有运行实例
    • 创建唯一token和日志文件
    • 写入并启动多路复用服务器脚本
    • 监听本地端口,处理多个连接
  2. Code服务器:

    • 检查现有进程状态
    • 生成连接token
    • 启动主要的VSCode服务器进程
    • 配置自动关闭和遥测设置

🔍 状态监控

  1. 启动验证: 通过日志文件确认服务器成功启动并获取监听端口
  2. 结果输出: 使用特殊格式输出安装结果(包含端口、token等信息)
  3. 进程监控: 持续监控服务器进程,确保服务可用性

🔑 关键技术特点

  • SSH_AUTH_SOCK管理: 临时取消设置,防止SSH agent在所有连接中被共享
  • Token安全: 为每个服务器组件生成唯一的连接token
  • 容错处理: 多层错误检查和退出机制
  • 并发控制: 文件锁机制防止重复安装
  • 向后兼容: 检测并复用已安装的服务器实例

这个脚本设计精巧,考虑了远程安装的各种边界情况,确保Cursor能够在各种Linux环境中稳定运行,为用户提供无缝的远程开发体验。

posted @ 2025-06-05 17:14  ffl  阅读(6525)  评论(0)    收藏  举报