Skip to content

Commit 8524577

Browse files
author
neil
committed
support --sync-time
1 parent 5c3d808 commit 8524577

File tree

2 files changed

+172
-40
lines changed

2 files changed

+172
-40
lines changed

.github/workflows/testrun.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ jobs:
9090
echo "anyvm" > ${mountdir}/justcheck.txt
9191
echo "anyvm" > ${mountdir}/.dotfile
9292
mkdir -p ../test_output
93-
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 }}"
93+
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 }}" --sync-time
9494
# We use the ssh port as an alias to the vm, so we can use 'ssh $port' to login
9595
echo "===============test 1"
9696
${{ inputs.sleep }}

anyvm.py

Lines changed: 171 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -841,6 +841,8 @@ def fatal(msg):
841841
ctx.imageSmoothingEnabled = false;
842842
ctx.webkitImageSmoothingEnabled = false;
843843
844+
handleResize();
845+
844846
// Request updates as fast as possible
845847
requestUpdate(false);
846848
} else break;
@@ -1289,16 +1291,45 @@ def fatal(msg):
12891291
ctx.webkitImageSmoothingEnabled = false;
12901292
ctx.mozImageSmoothingEnabled = false;
12911293
1294+
let currentW = fbWidth;
12921295
if (document.fullscreenElement === canvas) {
12931296
const dpr = window.devicePixelRatio || 1;
12941297
const physicalWidth = window.innerWidth * dpr;
12951298
const physicalHeight = window.innerHeight * dpr;
12961299
const scale = Math.max(1, Math.min(Math.floor(physicalWidth / fbWidth), Math.floor(physicalHeight / fbHeight)));
1297-
canvas.style.width = (fbWidth * scale / dpr) + "px";
1300+
currentW = (fbWidth * scale / dpr);
1301+
canvas.style.width = currentW + "px";
12981302
canvas.style.height = (fbHeight * scale / dpr) + "px";
12991303
} else {
1300-
canvas.style.width = "";
1301-
canvas.style.height = "";
1304+
// VNC Scaling logic: scale up if smaller than area, cap at 1280x800
1305+
const vw = window.innerWidth - 60;
1306+
const vh = window.innerHeight - 120;
1307+
const maxW = 1280;
1308+
const maxH = 800;
1309+
1310+
const targetW = Math.min(vw, maxW);
1311+
const targetH = Math.min(vh, maxH);
1312+
1313+
const canvasRatio = fbWidth / fbHeight;
1314+
const targetRatio = targetW / targetH;
1315+
1316+
let w, h;
1317+
if (targetRatio > canvasRatio) {
1318+
h = targetH;
1319+
w = h * canvasRatio;
1320+
} else {
1321+
w = targetW;
1322+
h = w / canvasRatio;
1323+
}
1324+
1325+
currentW = Math.floor(w);
1326+
canvas.style.width = currentW + "px";
1327+
canvas.style.height = Math.floor(h) + "px";
1328+
}
1329+
1330+
if (connected && status) {
1331+
const zoom = (currentW / fbWidth).toFixed(1);
1332+
status.textContent = `Connected: ${fbWidth}X${fbHeight} (${zoom}X)`;
13021333
}
13031334
13041335
// Use ResizeObserver for reliability if not already set
@@ -1913,6 +1944,8 @@ def print_usage():
19131944
--console, -c Run QEMU in foreground (console mode).
19141945
--builder <ver> Specify a specific vmactions builder version tag.
19151946
--snapshot Enable QEMU snapshot mode (changes are not saved).
1947+
--sync-time [off] Synchronize VM time using NTP inside the guest after boot.
1948+
(Default: enabled for DragonFlyBSD/Solaris family, disabled otherwise).
19161949
-- Send all following args to the final ssh command (executes inside the VM).
19171950
--help, -h Show this help message.
19181951
@@ -2314,6 +2347,91 @@ def call_with_timeout(cmd, timeout_seconds, **popen_kwargs):
23142347
pass
23152348
return None, True
23162349

2350+
def sync_vm_time(config, ssh_base_cmd):
2351+
"""Synchronizes VM time using NTP-like commands inside the guest."""
2352+
guest_os = config.get('os', '').lower()
2353+
debug = config.get('debug')
2354+
2355+
def get_guest_time():
2356+
try:
2357+
# Try to get date with milliseconds
2358+
cmd = "date '+%Y-%m-%d %H:%M:%S.%3N'"
2359+
if guest_os in ['freebsd', 'openbsd', 'netbsd', 'dragonflybsd', 'solaris', 'omnios', 'openindiana', 'haiku']:
2360+
cmd = "date '+%Y-%m-%d %H:%M:%S.000'"
2361+
2362+
p = subprocess.Popen(ssh_base_cmd + [cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
2363+
out, _ = p.communicate()
2364+
if p.returncode == 0:
2365+
return out.decode('utf-8', errors='replace').strip()
2366+
except:
2367+
pass
2368+
return "unknown"
2369+
2370+
def format_host_time(t):
2371+
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(t)) + ".{:03d}".format(int((t % 1) * 1000))
2372+
2373+
host_now = time.time()
2374+
log("Host time: {}".format(format_host_time(host_now)))
2375+
2376+
time_before = get_guest_time()
2377+
log("VM time before sync: {}".format(time_before))
2378+
2379+
log("Syncing VM time for OS: {}".format(guest_os))
2380+
# Construct NTP-like sync commands based on OS
2381+
ntp_servers = "pool.ntp.org time.google.com"
2382+
sync_cmd = ""
2383+
2384+
if guest_os == 'openbsd':
2385+
# OpenBSD uses rdate -n for SNTP sync
2386+
major_ntp = ntp_servers.split()[0]
2387+
sync_cmd = "rdate -n {0} || rdate {0}".format(major_ntp)
2388+
elif guest_os == 'dragonflybsd':
2389+
# DragonflyBSD specific: dntpd is the native daemon and was confirmed to work.
2390+
sync_cmd = ("/usr/sbin/dntpd -s || dntpd -s || "
2391+
"/usr/sbin/ntpd -g -q || ntpd -g -q || /usr/sbin/ntpd -s || ntpd -s || "
2392+
"/usr/sbin/ntpdate -u {0} || /usr/bin/ntpdate -u {0} || "
2393+
"/usr/sbin/ntpdig -S {0} || /usr/bin/ntpdig -S {0} || "
2394+
"/usr/sbin/rdate time.nist.gov || /usr/bin/rdate time.nist.gov || rdate time.nist.gov").format(ntp_servers)
2395+
elif guest_os in ['freebsd', 'netbsd']:
2396+
# Try common BSD NTP tools with rdate fallback
2397+
sync_cmd = "ntpdate -u {0} || ntpdig -S {0} || sntp -sS {0} || rdate pool.ntp.org || rdate time.nist.gov".format(ntp_servers)
2398+
elif guest_os == 'omnios':
2399+
# OmniOS specific: rdate to time.nist.gov was confirmed to work in previous runs.
2400+
major_ntp = ntp_servers.split()[0]
2401+
sync_cmd = ("rdate time.nist.gov || /usr/bin/rdate time.nist.gov || /usr/sbin/rdate time.nist.gov || "
2402+
"/usr/sbin/ntp-setdate {0} || /usr/lib/inet/ntpdate -u {0} || /usr/sbin/ntpdate -u {0} || ntpdate -u {0} || "
2403+
"/usr/lib/inet/sntp -s {0} || /usr/bin/sntp -s {0} || sntp -s {0}").format(major_ntp)
2404+
elif guest_os in ['solaris', 'openindiana']:
2405+
# General Solaris-like systems
2406+
sync_cmd = "ntpdate -u {0} || sntp -sS {0}".format(ntp_servers)
2407+
elif guest_os == 'haiku':
2408+
# Haiku uses Time --update to sync with configured NTP servers
2409+
sync_cmd = "Time --update || ntpdate -u {0}".format(ntp_servers)
2410+
else:
2411+
# Linux default: try common tool chain
2412+
sync_cmd = "ntpdate -u {0} || sntp -sS {0} || chronyc -a makestep || timeout 5 pulse-sync || (timedatectl set-ntp false && timedatectl set-ntp true)".format(ntp_servers)
2413+
2414+
full_cmd = sync_cmd
2415+
debuglog(debug, "Attempting NTP sync inside VM...")
2416+
debuglog(debug, "NTP Sync Command: {}".format(full_cmd))
2417+
2418+
try:
2419+
# Increase timeout for NTP as network might be slow initially
2420+
ret, timed_out = call_with_timeout(
2421+
ssh_base_cmd + [full_cmd],
2422+
timeout_seconds=15,
2423+
stdout=None if debug else DEVNULL,
2424+
stderr=None if debug else DEVNULL
2425+
)
2426+
log("NTP sync finished (ret={}, timeout={})".format(ret, timed_out))
2427+
except Exception as e:
2428+
log("NTP sync failed with exception: {}".format(e))
2429+
pass
2430+
2431+
time_after = get_guest_time()
2432+
log("VM time after sync: {}".format(time_after))
2433+
log("Host time: {}".format(format_host_time(time.time())))
2434+
23172435
def create_sized_file(path, size_mb):
23182436
"""Creates a zero-filled file of size_mb."""
23192437
chunk_size = 1024 * 1024 # 1MB
@@ -2848,7 +2966,8 @@ def main():
28482966
'cachedir': "",
28492967
'vga': "",
28502968
'resolution': "1280x800",
2851-
'snapshot': False
2969+
'snapshot': False,
2970+
'synctime': None
28522971
}
28532972

28542973
ssh_passthrough = []
@@ -2966,6 +3085,12 @@ def main():
29663085
i += 1
29673086
elif arg == "--snapshot":
29683087
config['snapshot'] = True
3088+
elif arg == "--sync-time":
3089+
if i + 1 < len(args) and args[i+1] == "off":
3090+
config['synctime'] = False
3091+
i += 1
3092+
else:
3093+
config['synctime'] = True
29693094
i += 1
29703095

29713096
if config['debug']:
@@ -3388,41 +3513,39 @@ def find_image_link(releases, target_zst, target_xz):
33883513
else:
33893514
addr = "127.0.0.1"
33903515

3516+
# Ensure serial port is allocated for background logging and VNC console
3517+
if not config['serialport']:
3518+
serial_port = get_free_port(start=7000, end=9000)
3519+
if not serial_port:
3520+
fatal("No free serial ports available")
3521+
config['serialport'] = str(serial_port)
3522+
33913523
if serial_user_specified:
33923524
serial_bind_addr = "0.0.0.0" if config['public'] else "127.0.0.1"
33933525
else:
33943526
serial_bind_addr = "127.0.0.1"
33953527

3396-
serial_chardev_def = None
3397-
serial_log_file = None
3398-
3399-
if is_vnc_console or serial_user_specified:
3400-
if not config['serialport']:
3401-
serial_port = get_free_port(start=7000, end=9000)
3402-
if not serial_port:
3403-
fatal("No free serial ports available")
3404-
config['serialport'] = str(serial_port)
3405-
3406-
if config['debug']:
3407-
serial_log_file = os.path.join(output_dir, "{}.serial.log".format(vm_name))
3408-
if os.path.exists(serial_log_file):
3409-
try:
3410-
os.remove(serial_log_file)
3411-
except:
3412-
pass
3413-
3414-
serial_chardev_id = "serial0"
3415-
serial_chardev_def = "socket,id={},host={},port={},server=on,wait=off,logfile={}".format(
3416-
serial_chardev_id, serial_bind_addr, config['serialport'], serial_log_file)
3417-
serial_arg = "chardev:{}".format(serial_chardev_id)
3418-
else:
3419-
serial_arg = "tcp:{}:{},server,nowait".format(serial_bind_addr, config['serialport'])
3528+
# Always prepare serial log file
3529+
serial_log_file = os.path.join(output_dir, "{}.serial.log".format(vm_name))
3530+
if os.path.exists(serial_log_file):
3531+
try:
3532+
os.remove(serial_log_file)
3533+
except:
3534+
pass
3535+
3536+
serial_chardev_id = "serial0"
3537+
serial_chardev_def = "socket,id={},host={},port={},server=on,wait=off,logfile={}".format(
3538+
serial_chardev_id, serial_bind_addr, config['serialport'], serial_log_file)
3539+
3540+
# Default to using this log-enabled chardev
3541+
serial_arg = "chardev:{}".format(serial_chardev_id)
34203542

3421-
debuglog(config['debug'],"Serial console listening on {}:{} (tcp)".format(serial_bind_addr, config['serialport']))
3422-
elif config['console']:
3543+
if config['console']:
3544+
# For foreground console mode, prioritize stdio interaction
34233545
serial_arg = "mon:stdio"
3424-
else:
3425-
serial_arg = "none"
3546+
3547+
debuglog(config['debug'], "Serial console logging to: " + serial_log_file)
3548+
debuglog(config['debug'], "Serial console listening on {}:{} (tcp)".format(serial_bind_addr, config['serialport']))
34263549

34273550
# QEMU Construction
34283551
bin_name = "qemu-system-x86_64"
@@ -3452,13 +3575,10 @@ def find_image_link(releases, target_zst, target_xz):
34523575
config['vnc'] = 'console'
34533576
is_vnc_console = True
34543577

3455-
# If we previously defaulted to stdio or disabled serial, we must switch to TCP serial
3456-
if serial_arg in ["mon:stdio", "none"]:
3457-
if not config['serialport']:
3458-
config['serialport'] = str(get_free_port(start=7000, end=9000))
3459-
# In auto-enabling case, serial_bind_addr is already 127.0.0.1 because serial_user_specified is False
3460-
serial_arg = "tcp:{}:{},server,nowait".format(serial_bind_addr, config['serialport'])
3461-
debuglog(config['debug'], "Switched serial to TCP for VNC Console: " + serial_arg)
3578+
# If we previously defaulted to stdio, we must switch back to the log-enabled chardev for VNC compatibility
3579+
if serial_arg == "mon:stdio":
3580+
serial_arg = "chardev:{}".format(serial_chardev_id)
3581+
debuglog(config['debug'], "Switched serial back to chardev for VNC Console compatibility: " + serial_arg)
34623582

34633583
# Acceleration determination
34643584
accel = "tcg"
@@ -4289,6 +4409,18 @@ def supports_ansi_color(stream):
42894409
qemu_elapsed = time.time() - qemu_start_time
42904410
debuglog(config['debug'], "VM Ready! Boot took {:.2f} seconds. Connect with: ssh {}".format(qemu_elapsed, vm_name))
42914411

4412+
# Sync VM time with host if requested
4413+
should_sync = config['synctime']
4414+
if should_sync is None:
4415+
# Default behavior: Only sync for DragonFlyBSD and Solaris family
4416+
if config['os'] in ['dragonflybsd', 'solaris', 'omnios', 'openindiana']:
4417+
should_sync = True
4418+
else:
4419+
should_sync = False
4420+
4421+
if should_sync:
4422+
sync_vm_time(config, ssh_base_cmd)
4423+
42924424
# Post-boot config: Setup reverse SSH config inside VM
42934425
current_user = getpass.getuser()
42944426
host_port_line = ""

0 commit comments

Comments
 (0)