@@ -73,14 +73,14 @@ 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" ,
8080 "solaris" : "2.0.0" ,
8181 "omnios" : "2.0.3" ,
8282 "haiku" : "0.0.2" ,
83- "openindiana" : "2.0.1 "
83+ "openindiana" : "2.0.2 "
8484}
8585
8686VERSION_TOKEN_RE = re .compile (r"[0-9]+|[A-Za-z]+" )
@@ -2200,34 +2200,148 @@ 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 ):
2204- """Syncs a host directory to the guest using rsync (Pull mode)."""
2203+ def sync_rsync (ssh_cmd , vhost , vguest , os_name , output_dir , vm_name ):
2204+ """Syncs a host directory to the guest using rsync (Push mode)."""
22052205 host_rsync = find_rsync ()
2206- if IS_WINDOWS and not host_rsync :
2206+ if not host_rsync :
22072207 log ("Warning: rsync not found on host. Install rsync to use rsync sync mode." )
22082208 return
22092209
2210+ # 1. Ensure destination directory exists in guest
2211+ try :
2212+ # Use a simpler check for directory existence and creation
2213+ p = subprocess .Popen (ssh_cmd + ["mkdir -p \" {}\" " .format (vguest )], stdout = DEVNULL , stderr = DEVNULL )
2214+ p .wait (timeout = 10 )
2215+ except Exception :
2216+ pass
2217+
22102218 log ("Syncing via rsync: {} -> {}" .format (vhost , vguest ))
2211- rsync_path_arg = '--rsync-path="{}"' .format (host_rsync .replace ("\\ " , "/" ))
22122219
2213- mount_script = """
2214- mkdir -p "{vguest}"
2215- if command -v rsync >/dev/null 2>&1; then
2216- rsync -avrtopg --delete {rsync_path} host:"{vhost}/" "{vguest}/"
2217- else
2218- echo "Error: rsync not found in guest."
2219- exit 1
2220- fi
2221- """ .format (vguest = vguest , vhost = vhost , rsync_path = rsync_path_arg )
2220+ if not ssh_cmd or len (ssh_cmd ) < 2 :
2221+ return
2222+
2223+ # Extract destination from ssh_cmd (last element)
2224+ remote_host = ssh_cmd [- 1 ]
2225+
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
2268+
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
22222310
2311+ if os .path .isdir (vhost ) and not src .endswith ('/' ):
2312+ src += "/"
2313+
2314+ # Build rsync command
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 ]
2318+
2319+ # Specify remote rsync path as it might not be in default non-interactive PATH.
2320+ # These MUST come before the source/destination arguments.
2321+ if os_name == "freebsd" :
2322+ cmd .extend (["--rsync-path" , "/usr/local/bin/rsync" ])
2323+ elif os_name in ["openindiana" , "solaris" , "omnios" ]:
2324+ cmd .extend (["--rsync-path" , "/usr/bin/rsync" ])
2325+
2326+ # Source and Destination come last
2327+ cmd .extend ([src , "{}:{}" .format (remote_host , vguest )])
2328+
2329+ debuglog (True , "Full rsync command: {}" .format (" " .join (cmd )))
2330+
22232331 synced = False
2224- for _ in range (10 ):
2225- p_sync = subprocess .Popen (ssh_cmd + ["sh" ], stdin = subprocess .PIPE )
2226- p_sync .communicate (input = mount_script .encode ('utf-8' ))
2227- if p_sync .returncode == 0 :
2228- synced = True
2229- break
2230- log ("Rsync sync failed, retrying..." )
2332+ # Attempt sync with retries
2333+ for i in range (10 ):
2334+ try :
2335+ # On Windows, Popen with explicit wait works best for rsync child processes
2336+ p = subprocess .Popen (cmd )
2337+ p .wait ()
2338+ if p .returncode == 0 :
2339+ synced = True
2340+ break
2341+ except Exception as e :
2342+ debuglog (True , "Rsync execution error: {}" .format (e ))
2343+
2344+ log ("Rsync sync failed, retrying ({})..." .format (i + 1 ))
22312345 time .sleep (2 )
22322346
22332347 if not synced :
@@ -3822,7 +3936,7 @@ def supports_ansi_color(stream):
38223936 if config ['sync' ] == 'nfs' :
38233937 sync_nfs (ssh_base_cmd , vhost , vguest , config ['os' ], sudo_cmd )
38243938 elif config ['sync' ] == 'rsync' :
3825- sync_rsync (ssh_base_cmd , vhost , vguest , config ['os' ])
3939+ sync_rsync (ssh_base_cmd , vhost , vguest , config ['os' ], output_dir , vm_name )
38263940 elif config ['sync' ] == 'scp' :
38273941 sync_scp (ssh_base_cmd , vhost , vguest , config ['sshport' ], hostid_file , vm_user )
38283942 else :
0 commit comments