fix: Windows Chrome extraction and debugging environment (#1088)
* windows debugging
* fixes
* fixes
* fix: handle Windows path separators in Chrome zip extraction
The zip crate's enclosed_name() normalizes paths to use backslashes on
Windows, but extract_zip used split_once('/') which only matches forward
slashes. This caused Chrome to be extracted into a nested chrome-win64/
subdirectory instead of directly into the version directory.
Also adds debug diagnostics to find_installed_chrome() (gated behind
AGENT_BROWSER_DEBUG) and better error messages when Chrome cache exists
but no binary is found.
Fixes #1076
* feat: add Puppeteer browser cache as Chrome fallback
Search ~/.cache/puppeteer/chrome/ (or PUPPETEER_CACHE_DIR) for Chrome
binaries before falling back to Playwright's cache. Puppeteer v19+
stores Chrome for Testing in this location, so users with an existing
Puppeteer install can use agent-browser without a separate install step.
* fmt
This commit is contained in:
@@ -46,6 +46,9 @@ yarn.lock
|
|||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
|
|
||||||
|
# Windows debug instance config
|
||||||
|
scripts/windows-debug/.instance
|
||||||
|
|
||||||
# opensrc - source code for packages
|
# opensrc - source code for packages
|
||||||
opensrc/
|
opensrc/
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,68 @@ cd cli && cargo fmt -- --check # Check formatting
|
|||||||
cd cli && cargo clippy # Lint
|
cd cli && cargo clippy # Lint
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Windows Debugging
|
||||||
|
|
||||||
|
A remote Windows Server 2022 EC2 instance is available for debugging Windows-specific issues. It uses AWS Systems Manager (SSM) -- no SSH, no open ports. Commands run via `aws ssm send-command` and return stdout/stderr.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
The instance must be provisioned first (one-time, by a human):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/windows-debug/provision.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires: AWS CLI v2 configured with `ec2:*`, `iam:CreateRole`, `iam:AttachRolePolicy`, `ssm:SendCommand`, `ssm:GetCommandInvocation` permissions and a default VPC.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
Start the instance (if stopped):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/windows-debug/start.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Run a command on Windows:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/windows-debug/run.sh "<powershell-command>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Sync the current git branch and rebuild:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/windows-debug/sync.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Stop the instance when done (avoids cost):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/windows-debug/stop.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Workflows
|
||||||
|
|
||||||
|
Run unit tests on Windows:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/windows-debug/run.sh "cd C:\agent-browser && cargo test --manifest-path cli\Cargo.toml"
|
||||||
|
```
|
||||||
|
|
||||||
|
Run e2e tests on Windows:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/windows-debug/run.sh "cd C:\agent-browser && cargo test e2e --manifest-path cli\Cargo.toml -- --ignored --test-threads=1"
|
||||||
|
```
|
||||||
|
|
||||||
|
Check bootstrap progress (first boot only):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/windows-debug/run.sh "Get-Content C:\bootstrap.log"
|
||||||
|
```
|
||||||
|
|
||||||
|
The repo lives at `C:\agent-browser` on the instance. Rust, Git, and Chrome are pre-installed. The `run.sh` wrapper automatically adds cargo and git to PATH.
|
||||||
|
|
||||||
<!-- opensrc:start -->
|
<!-- opensrc:start -->
|
||||||
|
|
||||||
## Source Code Reference
|
## Source Code Reference
|
||||||
|
|||||||
+67
-8
@@ -16,30 +16,85 @@ pub fn get_browsers_dir() -> PathBuf {
|
|||||||
|
|
||||||
pub fn find_installed_chrome() -> Option<PathBuf> {
|
pub fn find_installed_chrome() -> Option<PathBuf> {
|
||||||
let browsers_dir = get_browsers_dir();
|
let browsers_dir = get_browsers_dir();
|
||||||
|
let debug = std::env::var("AGENT_BROWSER_DEBUG").is_ok();
|
||||||
|
|
||||||
|
if debug {
|
||||||
|
let _ = writeln!(
|
||||||
|
io::stderr(),
|
||||||
|
"[chrome-search] home_dir={:?} browsers_dir={}",
|
||||||
|
dirs::home_dir(),
|
||||||
|
browsers_dir.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if !browsers_dir.exists() {
|
if !browsers_dir.exists() {
|
||||||
|
if debug {
|
||||||
|
let _ = writeln!(io::stderr(), "[chrome-search] browsers_dir does not exist");
|
||||||
|
}
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut versions: Vec<_> = fs::read_dir(&browsers_dir)
|
let entries = match fs::read_dir(&browsers_dir) {
|
||||||
.ok()?
|
Ok(entries) => entries,
|
||||||
|
Err(e) => {
|
||||||
|
let _ = writeln!(
|
||||||
|
io::stderr(),
|
||||||
|
"Warning: cannot read Chrome cache directory {}: {}",
|
||||||
|
browsers_dir.display(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut versions: Vec<_> = entries
|
||||||
.filter_map(|e| e.ok())
|
.filter_map(|e| e.ok())
|
||||||
.filter(|e| {
|
.filter(|e| {
|
||||||
e.file_name()
|
let matches = e
|
||||||
|
.file_name()
|
||||||
.to_str()
|
.to_str()
|
||||||
.is_some_and(|n| n.starts_with("chrome-"))
|
.is_some_and(|n| n.starts_with("chrome-"));
|
||||||
|
if debug {
|
||||||
|
let _ = writeln!(
|
||||||
|
io::stderr(),
|
||||||
|
"[chrome-search] entry {:?} matches={}",
|
||||||
|
e.file_name(),
|
||||||
|
matches
|
||||||
|
);
|
||||||
|
}
|
||||||
|
matches
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
versions.sort_by_key(|b| std::cmp::Reverse(b.file_name()));
|
versions.sort_by_key(|b| std::cmp::Reverse(b.file_name()));
|
||||||
|
|
||||||
for entry in versions {
|
for entry in versions {
|
||||||
if let Some(bin) = chrome_binary_in_dir(&entry.path()) {
|
let dir = entry.path();
|
||||||
if bin.exists() {
|
if let Some(bin) = chrome_binary_in_dir(&dir) {
|
||||||
|
let exists = bin.exists();
|
||||||
|
if debug {
|
||||||
|
let _ = writeln!(
|
||||||
|
io::stderr(),
|
||||||
|
"[chrome-search] candidate {} exists={}",
|
||||||
|
bin.display(),
|
||||||
|
exists
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
return Some(bin);
|
return Some(bin);
|
||||||
}
|
}
|
||||||
|
} else if debug {
|
||||||
|
let _ = writeln!(
|
||||||
|
io::stderr(),
|
||||||
|
"[chrome-search] no binary found in {}",
|
||||||
|
dir.display()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if debug {
|
||||||
|
let _ = writeln!(io::stderr(), "[chrome-search] no installed Chrome found");
|
||||||
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,10 +280,14 @@ fn extract_zip(bytes: Vec<u8>, dest: &Path) -> Result<(), String> {
|
|||||||
None => continue,
|
None => continue,
|
||||||
};
|
};
|
||||||
let raw_name = enclosed.to_string_lossy().to_string();
|
let raw_name = enclosed.to_string_lossy().to_string();
|
||||||
|
// Strip the top-level "chrome-<platform>/" directory from zip entries.
|
||||||
|
// On Windows, enclosed_name() normalizes paths to backslashes, so we
|
||||||
|
// must split on either separator.
|
||||||
let rel_path = raw_name
|
let rel_path = raw_name
|
||||||
.strip_prefix("chrome-")
|
.strip_prefix("chrome-")
|
||||||
.and_then(|s| s.split_once('/'))
|
.and_then(|s| s.find(['/', '\\']).map(|i| &s[i + 1..]))
|
||||||
.map(|(_, rest)| rest.to_string())
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(|s| s.to_string())
|
||||||
.unwrap_or(raw_name.clone());
|
.unwrap_or(raw_name.clone());
|
||||||
|
|
||||||
if rel_path.is_empty() {
|
if rel_path.is_empty() {
|
||||||
|
|||||||
@@ -219,9 +219,18 @@ fn build_chrome_args(options: &LaunchOptions) -> Result<ChromeArgs, String> {
|
|||||||
pub fn launch_chrome(options: &LaunchOptions) -> Result<ChromeProcess, String> {
|
pub fn launch_chrome(options: &LaunchOptions) -> Result<ChromeProcess, String> {
|
||||||
let chrome_path = match &options.executable_path {
|
let chrome_path = match &options.executable_path {
|
||||||
Some(p) => PathBuf::from(p),
|
Some(p) => PathBuf::from(p),
|
||||||
None => {
|
None => find_chrome().ok_or_else(|| {
|
||||||
find_chrome().ok_or("Chrome not found. Run `agent-browser install` to download Chrome, or use --executable-path.")?
|
let cache_dir = crate::install::get_browsers_dir();
|
||||||
}
|
format!(
|
||||||
|
"Chrome not found. Checked:\n \
|
||||||
|
- agent-browser cache: {}\n \
|
||||||
|
- System Chrome installations\n \
|
||||||
|
- Puppeteer browser cache\n \
|
||||||
|
- Playwright browser cache\n\
|
||||||
|
Run `agent-browser install` to download Chrome, or use --executable-path.",
|
||||||
|
cache_dir.display()
|
||||||
|
)
|
||||||
|
})?,
|
||||||
};
|
};
|
||||||
|
|
||||||
let max_attempts = 3;
|
let max_attempts = 3;
|
||||||
@@ -438,6 +447,18 @@ pub fn find_chrome() -> Option<PathBuf> {
|
|||||||
return Some(p);
|
return Some(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the cache directory exists but no Chrome was found, warn -- this
|
||||||
|
// likely means the cache is corrupted or the directory layout is unexpected.
|
||||||
|
let cache_dir = crate::install::get_browsers_dir();
|
||||||
|
if cache_dir.exists() {
|
||||||
|
let _ = writeln!(
|
||||||
|
std::io::stderr(),
|
||||||
|
"Warning: Chrome cache directory exists ({}) but no Chrome binary found inside. \
|
||||||
|
Falling back to system Chrome. Run `agent-browser install` to re-download.",
|
||||||
|
cache_dir.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Check system-installed Chrome
|
// 2. Check system-installed Chrome
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
@@ -502,7 +523,10 @@ pub fn find_chrome() -> Option<PathBuf> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Fallback: check Playwright's browser cache (for existing installs)
|
// 3. Fallback: check Puppeteer / Playwright browser caches
|
||||||
|
if let Some(p) = find_puppeteer_chrome() {
|
||||||
|
return Some(p);
|
||||||
|
}
|
||||||
if let Some(p) = find_playwright_chromium() {
|
if let Some(p) = find_playwright_chromium() {
|
||||||
return Some(p);
|
return Some(p);
|
||||||
}
|
}
|
||||||
@@ -684,6 +708,76 @@ fn should_disable_dev_shm(existing_args: &[String]) -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Search Puppeteer's browser cache for a Chrome binary.
|
||||||
|
/// Puppeteer v19+ stores Chrome in ~/.cache/puppeteer/chrome/<platform>-<version>/
|
||||||
|
fn find_puppeteer_chrome() -> Option<PathBuf> {
|
||||||
|
let mut search_dirs = Vec::new();
|
||||||
|
|
||||||
|
if let Ok(custom) = std::env::var("PUPPETEER_CACHE_DIR") {
|
||||||
|
search_dirs.push(PathBuf::from(custom).join("chrome"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(home) = dirs::home_dir() {
|
||||||
|
search_dirs.push(home.join(".cache/puppeteer/chrome"));
|
||||||
|
}
|
||||||
|
|
||||||
|
for dir in &search_dirs {
|
||||||
|
if !dir.is_dir() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Ok(entries) = std::fs::read_dir(dir) {
|
||||||
|
let mut matches: Vec<PathBuf> = entries
|
||||||
|
.filter_map(|e| e.ok())
|
||||||
|
.filter(|e| e.path().is_dir())
|
||||||
|
.filter_map(|e| {
|
||||||
|
let candidate = build_puppeteer_binary_path(&e.path());
|
||||||
|
if candidate.exists() {
|
||||||
|
Some(candidate)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
matches.sort();
|
||||||
|
matches.reverse();
|
||||||
|
if let Some(p) = matches.into_iter().next() {
|
||||||
|
return Some(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn build_puppeteer_binary_path(version_dir: &Path) -> PathBuf {
|
||||||
|
version_dir.join("chrome-linux64/chrome")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn build_puppeteer_binary_path(version_dir: &Path) -> PathBuf {
|
||||||
|
// Puppeteer uses chrome-mac-arm64 or chrome-mac-x64 depending on arch
|
||||||
|
let arm = version_dir.join(
|
||||||
|
"chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing",
|
||||||
|
);
|
||||||
|
if arm.exists() {
|
||||||
|
return arm;
|
||||||
|
}
|
||||||
|
version_dir.join(
|
||||||
|
"chrome-mac-x64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn build_puppeteer_binary_path(version_dir: &Path) -> PathBuf {
|
||||||
|
version_dir.join(r"chrome-win64\chrome.exe")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||||
|
fn build_puppeteer_binary_path(version_dir: &Path) -> PathBuf {
|
||||||
|
version_dir.join("chrome")
|
||||||
|
}
|
||||||
|
|
||||||
/// Search Playwright's browser cache for a Chromium binary.
|
/// Search Playwright's browser cache for a Chromium binary.
|
||||||
/// Legacy fallback for users who previously installed Chromium via Playwright.
|
/// Legacy fallback for users who previously installed Chromium via Playwright.
|
||||||
fn find_playwright_chromium() -> Option<PathBuf> {
|
fn find_playwright_chromium() -> Option<PathBuf> {
|
||||||
|
|||||||
Executable
+220
@@ -0,0 +1,220 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
INSTANCE_FILE="$SCRIPT_DIR/.instance"
|
||||||
|
NAME_PREFIX="agent-browser-debug"
|
||||||
|
INSTANCE_TYPE="${INSTANCE_TYPE:-t3.xlarge}"
|
||||||
|
|
||||||
|
if [[ -f "$INSTANCE_FILE" ]]; then
|
||||||
|
echo "Error: Instance already provisioned. See $INSTANCE_FILE"
|
||||||
|
echo "Run ./scripts/windows-debug/start.sh to start it, or delete .instance to re-provision."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
REGION=$(aws configure get region 2>/dev/null || echo "")
|
||||||
|
if [[ -z "$REGION" ]]; then
|
||||||
|
echo "Error: No AWS region configured. Run: aws configure set region us-east-1"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Provisioning Windows debug instance in $REGION..."
|
||||||
|
|
||||||
|
# --- IAM Role for SSM ---
|
||||||
|
ROLE_NAME="${IAM_ROLE_NAME:-$NAME_PREFIX-ssm-role}"
|
||||||
|
PROFILE_NAME="${INSTANCE_PROFILE_NAME:-$NAME_PREFIX-instance-profile}"
|
||||||
|
|
||||||
|
if aws iam get-instance-profile --instance-profile-name "$PROFILE_NAME" &>/dev/null; then
|
||||||
|
echo "Instance profile $PROFILE_NAME already exists, reusing."
|
||||||
|
else
|
||||||
|
echo "Instance profile $PROFILE_NAME not found. Creating IAM resources..."
|
||||||
|
|
||||||
|
if ! aws iam get-role --role-name "$ROLE_NAME" &>/dev/null; then
|
||||||
|
echo "Creating IAM role: $ROLE_NAME"
|
||||||
|
if ! aws iam create-role \
|
||||||
|
--role-name "$ROLE_NAME" \
|
||||||
|
--assume-role-policy-document '{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Principal": {"Service": "ec2.amazonaws.com"},
|
||||||
|
"Action": "sts:AssumeRole"
|
||||||
|
}]
|
||||||
|
}' \
|
||||||
|
--no-cli-pager; then
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Error: Failed to create IAM role (see error above)."
|
||||||
|
echo ""
|
||||||
|
echo "Ask an IAM admin to create the following, then re-run with:"
|
||||||
|
echo " INSTANCE_PROFILE_NAME=<name> ./scripts/windows-debug/provision.sh"
|
||||||
|
echo ""
|
||||||
|
echo "What the admin needs to create:"
|
||||||
|
echo " 1. IAM Role: $ROLE_NAME"
|
||||||
|
echo " - Trusted entity: EC2 (ec2.amazonaws.com)"
|
||||||
|
echo " - Attached policy: AmazonSSMManagedInstanceCore"
|
||||||
|
echo " 2. Instance Profile: $PROFILE_NAME"
|
||||||
|
echo " - With the above role added to it"
|
||||||
|
echo ""
|
||||||
|
echo "Or run these commands with an account that has iam:CreateRole permission:"
|
||||||
|
echo ""
|
||||||
|
echo " aws iam create-role --role-name $ROLE_NAME \\"
|
||||||
|
echo " --assume-role-policy-document '{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"ec2.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}'"
|
||||||
|
echo ""
|
||||||
|
echo " aws iam attach-role-policy --role-name $ROLE_NAME \\"
|
||||||
|
echo " --policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
|
||||||
|
echo ""
|
||||||
|
echo " aws iam create-instance-profile --instance-profile-name $PROFILE_NAME"
|
||||||
|
echo ""
|
||||||
|
echo " aws iam add-role-to-instance-profile \\"
|
||||||
|
echo " --instance-profile-name $PROFILE_NAME --role-name $ROLE_NAME"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
aws iam attach-role-policy \
|
||||||
|
--role-name "$ROLE_NAME" \
|
||||||
|
--policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
|
||||||
|
else
|
||||||
|
echo "IAM role $ROLE_NAME already exists."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Creating instance profile: $PROFILE_NAME"
|
||||||
|
aws iam create-instance-profile --instance-profile-name "$PROFILE_NAME" --no-cli-pager
|
||||||
|
aws iam add-role-to-instance-profile \
|
||||||
|
--instance-profile-name "$PROFILE_NAME" \
|
||||||
|
--role-name "$ROLE_NAME"
|
||||||
|
echo "Waiting for instance profile propagation..."
|
||||||
|
sleep 10
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Security Group (no inbound rules) ---
|
||||||
|
VPC_ID=$(aws ec2 describe-vpcs --filters "Name=isDefault,Values=true" --query "Vpcs[0].VpcId" --output text)
|
||||||
|
if [[ "$VPC_ID" == "None" || -z "$VPC_ID" ]]; then
|
||||||
|
echo "Error: No default VPC found. Create one with: aws ec2 create-default-vpc"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SG_NAME="$NAME_PREFIX-sg"
|
||||||
|
SG_ID=$(aws ec2 describe-security-groups \
|
||||||
|
--filters "Name=group-name,Values=$SG_NAME" "Name=vpc-id,Values=$VPC_ID" \
|
||||||
|
--query "SecurityGroups[0].GroupId" --output text 2>/dev/null || echo "None")
|
||||||
|
|
||||||
|
if [[ "$SG_ID" == "None" || -z "$SG_ID" ]]; then
|
||||||
|
echo "Creating security group: $SG_NAME"
|
||||||
|
SG_ID=$(aws ec2 create-security-group \
|
||||||
|
--group-name "$SG_NAME" \
|
||||||
|
--description "agent-browser Windows debug instance (SSM only, no inbound)" \
|
||||||
|
--vpc-id "$VPC_ID" \
|
||||||
|
--query "GroupId" --output text)
|
||||||
|
|
||||||
|
# Revoke default egress isn't needed; SSM requires outbound HTTPS.
|
||||||
|
# No inbound rules -- SSM uses outbound connections only.
|
||||||
|
else
|
||||||
|
echo "Security group $SG_NAME ($SG_ID) already exists, reusing."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- AMI (latest Windows Server 2022) ---
|
||||||
|
AMI_ID=$(aws ssm get-parameter \
|
||||||
|
--name "/aws/service/ami-windows-latest/Windows_Server-2022-English-Full-Base" \
|
||||||
|
--query "Parameter.Value" --output text)
|
||||||
|
echo "Using AMI: $AMI_ID (Windows Server 2022)"
|
||||||
|
|
||||||
|
# --- UserData bootstrap script ---
|
||||||
|
USERDATA_FILE=$(mktemp)
|
||||||
|
trap "rm -f $USERDATA_FILE" EXIT
|
||||||
|
|
||||||
|
cat > "$USERDATA_FILE" <<'PWSH'
|
||||||
|
<powershell>
|
||||||
|
$ErrorActionPreference = "Continue"
|
||||||
|
$logFile = "C:\bootstrap.log"
|
||||||
|
|
||||||
|
function Log($msg) {
|
||||||
|
$ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||||
|
"$ts $msg" | Tee-Object -FilePath $logFile -Append
|
||||||
|
}
|
||||||
|
|
||||||
|
Log "--- Bootstrap starting ---"
|
||||||
|
|
||||||
|
# Install Git
|
||||||
|
Log "Installing Git..."
|
||||||
|
$gitInstaller = "$env:TEMP\git-installer.exe"
|
||||||
|
Invoke-WebRequest -Uri "https://github.com/git-for-windows/git/releases/download/v2.47.1.windows.2/Git-2.47.1.2-64-bit.exe" -OutFile $gitInstaller
|
||||||
|
Start-Process -FilePath $gitInstaller -ArgumentList "/VERYSILENT /NORESTART /NOCANCEL /SP- /CLOSEAPPLICATIONS /RESTARTAPPLICATIONS /COMPONENTS=`"icons,ext\reg\shellhere,assoc,assoc_sh`"" -Wait
|
||||||
|
$env:PATH = "C:\Program Files\Git\cmd;$env:PATH"
|
||||||
|
[Environment]::SetEnvironmentVariable("PATH", "C:\Program Files\Git\cmd;$([Environment]::GetEnvironmentVariable('PATH', 'Machine'))", "Machine")
|
||||||
|
Log "Git installed: $(git --version)"
|
||||||
|
|
||||||
|
# Install Rust
|
||||||
|
Log "Installing Rust..."
|
||||||
|
$rustupInit = "$env:TEMP\rustup-init.exe"
|
||||||
|
Invoke-WebRequest -Uri "https://win.rustup.rs/x86_64" -OutFile $rustupInit
|
||||||
|
Start-Process -FilePath $rustupInit -ArgumentList "-y --default-toolchain stable" -Wait
|
||||||
|
$env:PATH = "$env:USERPROFILE\.cargo\bin;$env:PATH"
|
||||||
|
[Environment]::SetEnvironmentVariable("PATH", "$env:USERPROFILE\.cargo\bin;$([Environment]::GetEnvironmentVariable('PATH', 'Machine'))", "Machine")
|
||||||
|
Log "Rust installed: $(rustc --version)"
|
||||||
|
|
||||||
|
# Install MSVC build tools (required for Rust on Windows)
|
||||||
|
Log "Installing Visual Studio Build Tools..."
|
||||||
|
$vsInstaller = "$env:TEMP\vs_buildtools.exe"
|
||||||
|
Invoke-WebRequest -Uri "https://aka.ms/vs/17/release/vs_buildtools.exe" -OutFile $vsInstaller
|
||||||
|
Start-Process -FilePath $vsInstaller -ArgumentList "--quiet --wait --norestart --nocache --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended" -Wait
|
||||||
|
Log "Build tools installed."
|
||||||
|
|
||||||
|
# Clone repo
|
||||||
|
Log "Cloning agent-browser..."
|
||||||
|
git clone https://github.com/vercel-labs/agent-browser.git C:\agent-browser
|
||||||
|
Set-Location C:\agent-browser
|
||||||
|
Log "Repo cloned."
|
||||||
|
|
||||||
|
# Build CLI
|
||||||
|
Log "Building agent-browser CLI..."
|
||||||
|
cargo build --release --manifest-path cli\Cargo.toml
|
||||||
|
Log "Build complete."
|
||||||
|
|
||||||
|
# Install Chrome
|
||||||
|
Log "Installing Chrome via agent-browser..."
|
||||||
|
.\cli\target\release\agent-browser.exe install
|
||||||
|
Log "Chrome installed."
|
||||||
|
|
||||||
|
Log "--- Bootstrap complete ---"
|
||||||
|
</powershell>
|
||||||
|
PWSH
|
||||||
|
|
||||||
|
# --- Launch instance ---
|
||||||
|
echo "Launching $INSTANCE_TYPE instance..."
|
||||||
|
INSTANCE_ID=$(aws ec2 run-instances \
|
||||||
|
--image-id "$AMI_ID" \
|
||||||
|
--instance-type "$INSTANCE_TYPE" \
|
||||||
|
--iam-instance-profile "Name=$PROFILE_NAME" \
|
||||||
|
--security-group-ids "$SG_ID" \
|
||||||
|
--user-data "file://$USERDATA_FILE" \
|
||||||
|
--block-device-mappings '[{"DeviceName":"/dev/sda1","Ebs":{"VolumeSize":80,"VolumeType":"gp3"}}]' \
|
||||||
|
--tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=$NAME_PREFIX}]" \
|
||||||
|
--metadata-options "HttpTokens=required" \
|
||||||
|
--query "Instances[0].InstanceId" --output text)
|
||||||
|
|
||||||
|
echo "Instance launched: $INSTANCE_ID"
|
||||||
|
|
||||||
|
# Save instance config
|
||||||
|
cat > "$INSTANCE_FILE" <<EOF
|
||||||
|
INSTANCE_ID=$INSTANCE_ID
|
||||||
|
REGION=$REGION
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Waiting for instance to enter running state..."
|
||||||
|
aws ec2 wait instance-running --instance-ids "$INSTANCE_ID"
|
||||||
|
echo "Instance is running."
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Instance $INSTANCE_ID is booting and bootstrapping (Rust, Git, Chrome)."
|
||||||
|
echo "Bootstrap takes ~15-20 minutes on first boot."
|
||||||
|
echo ""
|
||||||
|
echo "Check bootstrap progress:"
|
||||||
|
echo " ./scripts/windows-debug/run.sh \"Get-Content C:\\bootstrap.log\""
|
||||||
|
echo ""
|
||||||
|
echo "Once ready, sync your branch and start debugging:"
|
||||||
|
echo " ./scripts/windows-debug/sync.sh"
|
||||||
|
echo " ./scripts/windows-debug/run.sh \"cd C:\\agent-browser && cargo test\""
|
||||||
|
echo ""
|
||||||
|
echo "Stop when done to save costs:"
|
||||||
|
echo " ./scripts/windows-debug/stop.sh"
|
||||||
Executable
+92
@@ -0,0 +1,92 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
INSTANCE_FILE="$SCRIPT_DIR/.instance"
|
||||||
|
|
||||||
|
if [[ ! -f "$INSTANCE_FILE" ]]; then
|
||||||
|
echo "Error: No instance provisioned. Run ./scripts/windows-debug/provision.sh first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $# -eq 0 ]]; then
|
||||||
|
echo "Usage: ./scripts/windows-debug/run.sh \"<powershell-command>\""
|
||||||
|
echo ""
|
||||||
|
echo "Examples:"
|
||||||
|
echo " ./scripts/windows-debug/run.sh \"cd C:\\agent-browser && cargo test\""
|
||||||
|
echo " ./scripts/windows-debug/run.sh \"Get-Content C:\\bootstrap.log\""
|
||||||
|
echo " ./scripts/windows-debug/run.sh \"cd C:\\agent-browser && cargo test e2e -- --ignored --test-threads=1\""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
source "$INSTANCE_FILE"
|
||||||
|
export AWS_DEFAULT_REGION="$REGION"
|
||||||
|
|
||||||
|
COMMAND="$*"
|
||||||
|
|
||||||
|
PARAMS_FILE=$(mktemp)
|
||||||
|
trap "rm -f $PARAMS_FILE" EXIT
|
||||||
|
|
||||||
|
python3 -c '
|
||||||
|
import json, sys
|
||||||
|
path_setup = "$env:PATH = \"$env:USERPROFILE\\.cargo\\bin;C:\\Program Files\\Git\\cmd;$env:PATH\""
|
||||||
|
cmd = path_setup + "\n" + sys.argv[1]
|
||||||
|
json.dump({"commands": [cmd]}, open(sys.argv[2], "w"))
|
||||||
|
' "$COMMAND" "$PARAMS_FILE"
|
||||||
|
|
||||||
|
COMMAND_ID=$(aws ssm send-command \
|
||||||
|
--instance-ids "$INSTANCE_ID" \
|
||||||
|
--document-name "AWS-RunPowerShellScript" \
|
||||||
|
--parameters "file://$PARAMS_FILE" \
|
||||||
|
--timeout-seconds 3600 \
|
||||||
|
--query "Command.CommandId" --output text)
|
||||||
|
|
||||||
|
echo "Command sent (ID: $COMMAND_ID). Waiting..." >&2
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
RESULT=$(aws ssm get-command-invocation \
|
||||||
|
--command-id "$COMMAND_ID" \
|
||||||
|
--instance-id "$INSTANCE_ID" \
|
||||||
|
--output json 2>&1) || true
|
||||||
|
|
||||||
|
STATUS=$(echo "$RESULT" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
try:
|
||||||
|
print(json.loads(sys.stdin.read()).get('Status', 'Unknown'))
|
||||||
|
except:
|
||||||
|
print('Pending')
|
||||||
|
" 2>/dev/null)
|
||||||
|
|
||||||
|
case "$STATUS" in
|
||||||
|
Success)
|
||||||
|
echo "$RESULT" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
r = json.loads(sys.stdin.read())
|
||||||
|
out = r.get('StandardOutputContent', '').rstrip()
|
||||||
|
err = r.get('StandardErrorContent', '').rstrip()
|
||||||
|
if out:
|
||||||
|
print(out)
|
||||||
|
if err:
|
||||||
|
print(err, file=sys.stderr)
|
||||||
|
"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
Failed|TimedOut|Cancelled)
|
||||||
|
echo "$RESULT" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
r = json.loads(sys.stdin.read())
|
||||||
|
out = r.get('StandardOutputContent', '').rstrip()
|
||||||
|
err = r.get('StandardErrorContent', '').rstrip()
|
||||||
|
if out:
|
||||||
|
print(out)
|
||||||
|
if err:
|
||||||
|
print(err, file=sys.stderr)
|
||||||
|
"
|
||||||
|
echo "Command $STATUS." >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
sleep 3
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
Executable
+43
@@ -0,0 +1,43 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
INSTANCE_FILE="$SCRIPT_DIR/.instance"
|
||||||
|
|
||||||
|
if [[ ! -f "$INSTANCE_FILE" ]]; then
|
||||||
|
echo "Error: No instance provisioned. Run ./scripts/windows-debug/provision.sh first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
source "$INSTANCE_FILE"
|
||||||
|
export AWS_DEFAULT_REGION="$REGION"
|
||||||
|
|
||||||
|
STATE=$(aws ec2 describe-instances \
|
||||||
|
--instance-ids "$INSTANCE_ID" \
|
||||||
|
--query "Reservations[0].Instances[0].State.Name" --output text)
|
||||||
|
|
||||||
|
if [[ "$STATE" == "running" ]]; then
|
||||||
|
echo "Instance $INSTANCE_ID is already running."
|
||||||
|
else
|
||||||
|
echo "Starting instance $INSTANCE_ID..."
|
||||||
|
aws ec2 start-instances --instance-ids "$INSTANCE_ID" --no-cli-pager
|
||||||
|
echo "Waiting for running state..."
|
||||||
|
aws ec2 wait instance-running --instance-ids "$INSTANCE_ID"
|
||||||
|
echo "Instance is running."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Waiting for SSM agent connectivity..."
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
SSM_STATUS=$(aws ssm describe-instance-information \
|
||||||
|
--filters "Key=InstanceIds,Values=$INSTANCE_ID" \
|
||||||
|
--query "InstanceInformationList[0].PingStatus" --output text 2>/dev/null || echo "None")
|
||||||
|
if [[ "$SSM_STATUS" == "Online" ]]; then
|
||||||
|
echo "SSM agent is online. Ready for commands."
|
||||||
|
echo " ./scripts/windows-debug/run.sh \"your-command-here\""
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Warning: SSM agent not online after 5 minutes. The instance may still be booting."
|
||||||
|
echo "Try again in a minute: ./scripts/windows-debug/run.sh \"hostname\""
|
||||||
Executable
+28
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
INSTANCE_FILE="$SCRIPT_DIR/.instance"
|
||||||
|
|
||||||
|
if [[ ! -f "$INSTANCE_FILE" ]]; then
|
||||||
|
echo "Error: No instance provisioned. Nothing to stop."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
source "$INSTANCE_FILE"
|
||||||
|
export AWS_DEFAULT_REGION="$REGION"
|
||||||
|
|
||||||
|
STATE=$(aws ec2 describe-instances \
|
||||||
|
--instance-ids "$INSTANCE_ID" \
|
||||||
|
--query "Reservations[0].Instances[0].State.Name" --output text)
|
||||||
|
|
||||||
|
if [[ "$STATE" == "stopped" ]]; then
|
||||||
|
echo "Instance $INSTANCE_ID is already stopped."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Stopping instance $INSTANCE_ID..."
|
||||||
|
aws ec2 stop-instances --instance-ids "$INSTANCE_ID" --no-cli-pager
|
||||||
|
echo "Waiting for stopped state..."
|
||||||
|
aws ec2 wait instance-stopped --instance-ids "$INSTANCE_ID"
|
||||||
|
echo "Instance stopped. No compute charges while stopped (storage only: ~$0.64/mo)."
|
||||||
Executable
+27
@@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
RUN="$SCRIPT_DIR/run.sh"
|
||||||
|
|
||||||
|
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "main")
|
||||||
|
REMOTE_URL=$(git remote get-url origin 2>/dev/null || echo "https://github.com/vercel-labs/agent-browser.git")
|
||||||
|
|
||||||
|
echo "Syncing branch '$BRANCH' on Windows instance..."
|
||||||
|
|
||||||
|
"$RUN" "
|
||||||
|
cd C:\agent-browser
|
||||||
|
git remote set-url origin '$REMOTE_URL'
|
||||||
|
git fetch origin
|
||||||
|
git checkout -B '$BRANCH' 'origin/$BRANCH'
|
||||||
|
git log -1 --oneline
|
||||||
|
"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Branch synced. Rebuilding..."
|
||||||
|
|
||||||
|
"$RUN" "
|
||||||
|
cd C:\agent-browser
|
||||||
|
cargo build --release --manifest-path cli\Cargo.toml
|
||||||
|
Write-Host 'Build complete.'
|
||||||
|
"
|
||||||
Reference in New Issue
Block a user