@@ -73,7 +73,7 @@ def urlretrieve(url, filename, reporthook=None):
7373
7474
7575DEFAULT_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