Skip to content

Commit 956b375

Browse files
author
neil
committed
fix rsync for windows
1 parent c6ac4ab commit 956b375

File tree

1 file changed

+106
-18
lines changed

1 file changed

+106
-18
lines changed

anyvm.py

Lines changed: 106 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def urlretrieve(url, filename, reporthook=None):
7373

7474

7575
DEFAULT_BUILDER_VERSIONS = {
76-
"freebsd": "2.0.4",
76+
"freebsd": "2.0.5",
7777
"openbsd": "2.0.0",
7878
"netbsd": "2.0.3",
7979
"dragonflybsd": "2.0.3",
@@ -2200,18 +2200,18 @@ def sync_nfs(ssh_cmd, vhost, vguest, os_name, sudo_cmd):
22002200
if not mounted:
22012201
log("Warning: Failed to mount shared folder via NFS.")
22022202

2203-
def sync_rsync(ssh_cmd, vhost, vguest, os_name):
2203+
def sync_rsync(ssh_cmd, vhost, vguest, os_name, output_dir, vm_name):
22042204
"""Syncs a host directory to the guest using rsync (Push mode)."""
22052205
host_rsync = find_rsync()
22062206
if not host_rsync:
22072207
log("Warning: rsync not found on host. Install rsync to use rsync sync mode.")
22082208
return
22092209

2210-
# Ensure destination directory exists in guest
2210+
# 1. Ensure destination directory exists in guest
22112211
try:
22122212
# Use a simpler check for directory existence and creation
22132213
p = subprocess.Popen(ssh_cmd + ["mkdir -p \"{}\"".format(vguest)], stdout=DEVNULL, stderr=DEVNULL)
2214-
p.wait()
2214+
p.wait(timeout=10)
22152215
except Exception:
22162216
pass
22172217

@@ -2220,34 +2220,122 @@ def sync_rsync(ssh_cmd, vhost, vguest, os_name):
22202220
if not ssh_cmd or len(ssh_cmd) < 2:
22212221
return
22222222

2223-
# Extract destination and SSH options
2223+
# Extract destination from ssh_cmd (last element)
22242224
remote_host = ssh_cmd[-1]
2225-
ssh_options = ssh_cmd[:-1]
22262225

2227-
# Build the SSH command string for rsync -e
2228-
# Note: On Windows, shlex.quote may use single quotes which some rsync versions don't like,
2229-
# but for Git Bash rsync it's typically fine.
2230-
ssh_opts_str = " ".join(shlex.quote(x) for x in ssh_options)
2226+
# On Windows, rsync -e commands are often executed by a mini-sh (part of msys2/git-bash).
2227+
# Using /dev/null is often safer than NUL in that context.
2228+
# We find the identity file path and port from the original ssh_cmd
2229+
ssh_port = "22"
2230+
id_file = None
2231+
i = 0
2232+
while i < len(ssh_cmd):
2233+
if ssh_cmd[i] == "-p" and i + 1 < len(ssh_cmd):
2234+
ssh_port = ssh_cmd[i+1]
2235+
elif ssh_cmd[i] == "-i" and i + 1 < len(ssh_cmd):
2236+
id_file = ssh_cmd[i+1].replace("\\", "/")
2237+
i += 1
2238+
2239+
# 0. Manage known_hosts file in output_dir
2240+
kh_path = os.path.join(output_dir, "{}.knownhosts".format(vm_name))
2241+
try:
2242+
# Clear or create the file
2243+
open(kh_path, 'w').close()
2244+
except Exception:
2245+
pass
2246+
2247+
# Find absolute path to ssh, prioritizing bundled tools
2248+
ssh_cmd_base = "ssh"
2249+
if IS_WINDOWS and host_rsync:
2250+
rsync_dir = os.path.dirname(os.path.abspath(host_rsync))
2251+
# Search relative to rsync executable:
2252+
# C:\ProgramData\chocolatey\bin\rsync.exe -> ../lib/rsync/tools/bin/ssh.exe
2253+
search_dirs = [
2254+
rsync_dir,
2255+
os.path.join(rsync_dir, "..", "tools", "bin"),
2256+
os.path.join(rsync_dir, "..", "lib", "rsync", "tools", "bin"),
2257+
os.path.join(rsync_dir, "tools", "bin")
2258+
]
2259+
for d in search_dirs:
2260+
candidate = os.path.join(d, "ssh.exe")
2261+
if os.path.exists(candidate):
2262+
# Use normalized Windows path with forward slashes.
2263+
# This is more compatible than /c/ style on various Windows rsync ports.
2264+
clean_path = os.path.normpath(candidate).replace("\\", "/")
2265+
ssh_cmd_base = '"{}"'.format(clean_path)
2266+
debuglog(True, "Using bundled SSH for rsync: {}".format(clean_path))
2267+
break
22312268

2232-
# Normalize source path for rsync
2233-
src = vhost.replace("\\", "/")
2269+
if ssh_cmd_base == "ssh":
2270+
debuglog(True, "Using system 'ssh' command for rsync.")
2271+
2272+
# Helper for path fields inside the -e string.
2273+
# Must be absolute for Windows SSH but handle separators correctly.
2274+
# Build a minimal, robust SSH string for rsync -e
2275+
# On Windows, within the rsync -e command string, we use Drive:/Path/Style
2276+
# but wrap them in quotes if they contain spaces or colons.
2277+
def to_ssh_path(p):
2278+
if IS_WINDOWS:
2279+
return os.path.abspath(p).replace("\\", "/")
2280+
return p
2281+
2282+
# Build a minimal, robust SSH string for rsync -e
2283+
# -T: Disable pseudo-terminal, -q: quiet, -o BatchMode=yes: no password prompt
2284+
ssh_parts = [
2285+
ssh_cmd_base,
2286+
"-T", "-q",
2287+
"-o", "BatchMode=yes",
2288+
"-o", "StrictHostKeyChecking=no",
2289+
"-o", "UserKnownHostsFile=\"{}\"".format(to_ssh_path(kh_path)),
2290+
"-p", ssh_port
2291+
]
2292+
if id_file:
2293+
ssh_parts.extend(["-i", "\"{}\"".format(to_ssh_path(id_file))])
2294+
2295+
ssh_opts_str = " ".join(ssh_parts)
2296+
2297+
# Normalize source path for rsync to avoid "double remote" error on Windows.
2298+
# We use a RELATIVE path here because relative paths don't have colons,
2299+
# thus preventing rsync from mistaking the drive letter for a remote hostname.
2300+
if IS_WINDOWS:
2301+
try:
2302+
# Try to get relative path from current working directory
2303+
src = os.path.relpath(vhost).replace("\\", "/")
2304+
except ValueError:
2305+
# Cross-drive case: we use absolute path with forward slashes.
2306+
# Note: Native Windows rsync might still struggle here if it sees a colon.
2307+
src = to_ssh_path(vhost)
2308+
else:
2309+
src = vhost
2310+
22342311
if os.path.isdir(vhost) and not src.endswith('/'):
22352312
src += "/"
22362313

22372314
# Build rsync command
2238-
# -a: archive, -v: verbose, -z: compress, -r: recursive, -t: times, -o: owner, -p: perms, -g: group
2239-
# We use -L to follow symlinks on the host.
2240-
cmd = [host_rsync, "-avrtopg", "-L", "--delete", "-e", ssh_opts_str, src, "{}:{}".format(remote_host, vguest)]
2315+
# -a: archive, -v: verbose, -r: recursive, -t: times, -o: owner, -p: perms, -g: group, -L: follow symlinks
2316+
# --blocking-io: Essential for Windows SSH pipes.
2317+
cmd = [host_rsync, "-avrtopg", "-L", "--blocking-io", "--delete", "-e", ssh_opts_str, src, "{}:{}".format(remote_host, vguest)]
2318+
2319+
# Specify remote rsync path as it might not be in default non-interactive PATH
2320+
if os_name == "freebsd":
2321+
cmd.extend(["--rsync-path", "/usr/local/bin/rsync"])
2322+
elif os_name in ["openindiana", "solaris", "omnios"]:
2323+
cmd.extend(["--rsync-path", "/usr/bin/rsync"])
2324+
2325+
debuglog(True, "Full rsync command: {}".format(" ".join(cmd)))
22412326

22422327
synced = False
22432328
# Attempt sync with retries
22442329
for i in range(10):
22452330
try:
2246-
if subprocess.call(cmd) == 0:
2331+
# On Windows, Popen with explicit wait works best for rsync child processes
2332+
p = subprocess.Popen(cmd)
2333+
p.wait()
2334+
if p.returncode == 0:
22472335
synced = True
22482336
break
22492337
except Exception as e:
2250-
debuglog(True, "Rsync error: {}".format(e))
2338+
debuglog(True, "Rsync execution error: {}".format(e))
22512339

22522340
log("Rsync sync failed, retrying ({})...".format(i+1))
22532341
time.sleep(2)
@@ -3844,7 +3932,7 @@ def supports_ansi_color(stream):
38443932
if config['sync'] == 'nfs':
38453933
sync_nfs(ssh_base_cmd, vhost, vguest, config['os'], sudo_cmd)
38463934
elif config['sync'] == 'rsync':
3847-
sync_rsync(ssh_base_cmd, vhost, vguest, config['os'])
3935+
sync_rsync(ssh_base_cmd, vhost, vguest, config['os'], output_dir, vm_name)
38483936
elif config['sync'] == 'scp':
38493937
sync_scp(ssh_base_cmd, vhost, vguest, config['sshport'], hostid_file, vm_user)
38503938
else:

0 commit comments

Comments
 (0)