@@ -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+
23172435def 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