Skip to content

Commit 43e287e

Browse files
authored
Merge pull request #2 from anyvm-org/dev
sync
2 parents 3638617 + 782c0c4 commit 43e287e

File tree

4 files changed

+158
-31
lines changed

4 files changed

+158
-31
lines changed

.github/workflows/testmacos.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
os: ["freebsd", "netbsd", "openbsd", "solaris", "dragonflybsd", "omnios", "openindiana"]
2626
arch: ["aarch64", "x86_64"]
2727
runs: ["macos-latest"]
28-
sync: ["scp"]
28+
sync: ["scp", "rsync"]
2929
exclude:
3030
- os: solaris
3131
arch: aarch64
@@ -44,6 +44,7 @@ jobs:
4444
os: ${{ matrix.os }}
4545
arch: ${{ matrix.arch }}
4646
sync: ${{ matrix.sync }}
47+
sleep: "sleep 5;"
4748

4849

4950

.github/workflows/testrun.yml

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ on:
3737
required: false
3838
type: string
3939
default: "/mnt/host"
40+
sleep:
41+
description: "sleep for test"
42+
required: false
43+
type: string
44+
default: ""
4045

4146

4247

@@ -73,7 +78,7 @@ jobs:
7378
if: runner.os == 'Windows'
7479
run: |
7580
#C:/msys64/usr/bin/pacman.exe -S --noconfirm mingw-w64-ucrt-x86_64-qemu
76-
choco install qemu zstandard
81+
choco install qemu zstandard rsync
7782
7883
- name: Test
7984
id: test
@@ -83,16 +88,23 @@ jobs:
8388
mountdir=test
8489
mkdir -p ${mountdir}
8590
echo "anyvm" > ${mountdir}/justcheck.txt
91+
echo "anyvm" > ${mountdir}/.dotfile
8692
mkdir -p ../test_output
8793
python anyvm.py --debug --data-dir ../test_output --os "${{ inputs.os }}" --release "${{ inputs.release }}" --arch "${{ inputs.arch }}" -d -v "${mountdir}:${{ inputs.vmpath }}" --ssh-port 10022 --ssh-name "testname" -p 20022:22 --nc "${{ inputs.nc }}" --sync "${{ inputs.sync }}"
8894
# We use the ssh port as an alias to the vm, so we can use 'ssh $port' to login
89-
echo "test 1"
90-
ssh -v 10022 ls ${{ inputs.vmpath }}
91-
echo "test 2"
95+
echo "===============test 1"
96+
${{ inputs.sleep }}
97+
ssh -v 10022 ls -lah ${{ inputs.vmpath }}
98+
echo "===============test 2"
99+
${{ inputs.sleep }}
92100
ssh -v 10022 ls ${{ inputs.vmpath }} | grep justcheck.txt
93-
echo "test 3"
101+
${{ inputs.sleep }}
102+
ssh -v 10022 ls -lah ${{ inputs.vmpath }} | grep '.dotfile'
103+
echo "===============test 3"
104+
${{ inputs.sleep }}
94105
ssh -v testname ls ${{ inputs.vmpath }} | grep justcheck.txt
95-
echo "test 4"
106+
echo "===============test 4"
107+
${{ inputs.sleep }}
96108
ssh -v -p 20022 -o StrictHostKeyChecking=accept-new ${{ inputs.user }}@localhost ls ${{ inputs.vmpath }} | grep justcheck.txt
97109
98110

.github/workflows/testwindows.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
os: ["freebsd", "netbsd", "openbsd", "solaris", "dragonflybsd", "omnios", "openindiana"]
2626
arch: [""]
2727
runs: ["windows-latest"]
28-
sync: ["scp"]
28+
sync: ["scp", "rsync"]
2929
exclude:
3030
- os: solaris
3131
arch: aarch64

anyvm.py

Lines changed: 137 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,14 @@ 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",
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

8686
VERSION_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

Comments
 (0)