【人类】重新安装 cursor 的 Remote-SSH,以及 Remote-SSH 的脚本做了什么
本文由博主编写
Cursor Remote-SSH 安装之前的重置/清理
问题:原来安装的是 VSCode 的 Remote-SSH,安装 Cursor 后不可用。或者 Cursor 升级到新版本后 Remote-SSH 不可用。
解决:彻底清理不可用版本的残留信息:
- 在远程主机上,查找到 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
- 本地卸载掉 Cursor ,特别是 Curosr 的个性化配置,在 Mac 下是
/Users/${user}/.cursor
的用户.cursor目录,注意这个目录里保留着已经安装的插件的数据。很多建议说只要卸载 Remote-SSH 即可,实际上会遇到很多问题,用彻底卸载是最快的方式重置。
全新 Cursor 安装
去 Cursor 官网下载最新版本,重新安装,安装后打开一个项目,看下 Curosr 的扩展里 Remore-SSH 确实被清理了。
使用 Cursor 打开一个远程主机
- 远程主机在本机配置了 ssh 密钥登陆
- 使用 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。以下是详细的逻辑解析:
🔧 初始化阶段
- 环境配置: 设置关键变量如
SERVER_COMMIT
、SERVER_DATA_DIR
、各种日志和token文件路径 - 平台检测: 识别操作系统(Linux/Darwin)和CPU架构(x64/arm64)
- 兼容性检查: 确保平台和架构被支持,特殊处理Alpine Linux
🔒 安装准备
- 目录创建: 在
~/.cursor-server/bin/{commit_hash}/
下创建安装目录 - 锁机制: 使用文件锁防止并发安装,确保安装过程的原子性
- URL构建: 根据平台和架构构建下载URL
📥 服务器下载与安装
- 智能下载: 优先使用
wget
,fallback 到curl
- 文件验证: 下载 tar.gz 文件并解压到指定目录
- 完整性检查: 验证关键文件(如
cursor-server
脚本)是否存在
⚙️ 环境配置
- Node.js检查: 验证bundled Node.js可执行性,如果失败则检查系统Node.js
- 版本要求: 确保Node.js版本≥20,必要时创建符号链接
- 清理工作: 删除旧版本的安装文件,保持系统整洁
🚀 服务启动
-
Multiplex服务器:
- 检查是否已有运行实例
- 创建唯一token和日志文件
- 写入并启动多路复用服务器脚本
- 监听本地端口,处理多个连接
-
Code服务器:
- 检查现有进程状态
- 生成连接token
- 启动主要的VSCode服务器进程
- 配置自动关闭和遥测设置
🔍 状态监控
- 启动验证: 通过日志文件确认服务器成功启动并获取监听端口
- 结果输出: 使用特殊格式输出安装结果(包含端口、token等信息)
- 进程监控: 持续监控服务器进程,确保服务可用性
🔑 关键技术特点
- SSH_AUTH_SOCK管理: 临时取消设置,防止SSH agent在所有连接中被共享
- Token安全: 为每个服务器组件生成唯一的连接token
- 容错处理: 多层错误检查和退出机制
- 并发控制: 文件锁机制防止重复安装
- 向后兼容: 检测并复用已安装的服务器实例
这个脚本设计精巧,考虑了远程安装的各种边界情况,确保Cursor能够在各种Linux环境中稳定运行,为用户提供无缝的远程开发体验。