Skip to content

Commit 5e9324e

Browse files
authored
Merge pull request #24 from anyvm-org/dev
sync
2 parents 630fd21 + 99d006c commit 5e9324e

2 files changed

Lines changed: 89 additions & 5 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ AnyVM includes a built-in, premium VNC Web UI that allows you to access the VM's
140140
- **Fullscreen**: Toggle fullscreen mode for an immersive experience.
141141
- **Stats**: Real-time FPS and latency monitoring.
142142
- **Accessibility**: Available at `http://localhost:6080` by default. If the port is occupied, AnyVM will automatically try the next available port (e.g., 6081, 6082).
143-
- **Remote Access**: Use `--remote-vnc` to automatically create a public, secure tunnel (via Cloudflare, Localhost.run, or Pinggy) to access your VM's display from anywhere in the world.
143+
- **Remote Access**: Use `--remote-vnc` to automatically create a public, secure tunnel (via Cloudflare, Localhost.run, or Pinggy) to access your VM's display from anywhere in the world. (In Google Cloud Shell, this is enabled by default; use `--remote-vnc no` to disable).
144144

145145
## 9. CLI options (with examples)
146146

@@ -240,6 +240,7 @@ All examples below use `python3 anyvm.py ...`. You can also run `python3 anyvm.p
240240
- `--remote-vnc`: Create a public tunnel for the VNC Web UI using Cloudflare, Localhost.run, or Pinggy.
241241
- Example: `python3 anyvm.py --os freebsd --remote-vnc`
242242
- Advanced: Use `cf`, `lhr`, or `pinggy` to specify a service: `python3 anyvm.py --os freebsd --remote-vnc cf`
243+
- Disable: Use `no` to disable (e.g., in Google Cloud Shell where it's default): `python3 anyvm.py --os freebsd --remote-vnc no`
243244

244245
- `--mon <port>`: Expose the QEMU monitor via telnet on localhost.
245246
- Example: `python3 anyvm.py --os freebsd --mon 4444`

anyvm.py

Lines changed: 87 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,11 @@ def quote(arg):
149149
def log(msg):
150150
t = time.time()
151151
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(t)) + ".{:03d}".format(int(t % 1 * 1000))
152+
if hasattr(sys.stdout, "isatty") and sys.stdout.isatty():
153+
# Clear current line (progress bar) and move cursor to beginning
154+
sys.stdout.write("\r\x1b[K")
152155
print("[{}] {}".format(timestamp, msg))
156+
sys.stdout.flush()
153157

154158
def supports_ansi_color(stream=sys.stdout):
155159
"""Checks if the stream supports ANSI color sequences."""
@@ -2384,6 +2388,8 @@ def print_usage():
23842388
Use "--vnc off" to disable.
23852389
--remote-vnc Create a public URL for the VNC Web UI using Cloudflare, Localhost.run, or Pinggy.
23862390
Usage: --remote-vnc (auto), --remote-vnc cf, --remote-vnc lhr, --remote-vnc pinggy.
2391+
Enabled by default if no local browser is detected (e.g., in Cloud Shell).
2392+
Use "--remote-vnc no" to disable.
23872393
--vga <type> VGA device type (e.g., virtio, std, virtio-gpu). Default: virtio (std for NetBSD).
23882394
--res, --resolution Set initial screen resolution (e.g., 1280x800). Default: 1280x800.
23892395
--mon <port> QEMU monitor telnet port (localhost).
@@ -3373,6 +3379,39 @@ def tail_serial_log(path, stop_event):
33733379
pass
33743380

33753381

3382+
def watch_vnc_tunnel_log(log_path, stop_event, is_default_notice=False):
3383+
"""Monitor VNC proxy log and print tunnel URL as soon as it appears."""
3384+
if not log_path:
3385+
return
3386+
start_wait = time.time()
3387+
while not os.path.exists(log_path):
3388+
if stop_event.is_set() or (time.time() - start_wait > 30):
3389+
return
3390+
time.sleep(0.5)
3391+
3392+
try:
3393+
with open(log_path, 'r') as f:
3394+
while not stop_event.is_set():
3395+
line = f.readline()
3396+
if not line:
3397+
time.sleep(0.5)
3398+
continue
3399+
match = re.search(r"Open this link to access WebVNC \(via ([^)]+)\): (https?://[^\s]+)", line)
3400+
if match:
3401+
service = match.group(1)
3402+
url = match.group(2)
3403+
display_url = url
3404+
if supports_ansi_color():
3405+
display_url = "\x1b[32m{}\x1b[0m".format(url)
3406+
log("Open this link to access WebVNC (via {}): {}".format(service, display_url))
3407+
if is_default_notice:
3408+
log("Notice: Remote VNC tunnel is enabled by default as no local browser was detected.")
3409+
log(" Use '--remote-vnc off' to disable it.")
3410+
return
3411+
except Exception:
3412+
pass
3413+
3414+
33763415
def detect_host_ssh_port(sshd_config_path="/etc/ssh/sshd_config"):
33773416
try:
33783417
with open(sshd_config_path, 'r') as f:
@@ -3460,7 +3499,8 @@ def main():
34603499
'public_vnc': False,
34613500
'public_ssh': False,
34623501
'accept_vm_ssh': False,
3463-
'remote_vnc': False
3502+
'remote_vnc': None,
3503+
'remote_vnc_is_default': False
34643504
}
34653505

34663506
ssh_passthrough = []
@@ -3477,6 +3517,8 @@ def main():
34773517
working_dir = "/tmp/anyvm.org"
34783518
if not os.path.exists(working_dir):
34793519
os.makedirs(working_dir)
3520+
config['remote_vnc'] = True
3521+
config['remote_vnc_is_default'] = True
34803522

34813523
# Manual argument parsing
34823524
args = sys.argv[1:]
@@ -3575,7 +3617,11 @@ def main():
35753617
config['public_ssh'] = True
35763618
elif arg == "--remote-vnc":
35773619
if i + 1 < len(args) and not args[i+1].startswith("-"):
3578-
config['remote_vnc'] = args[i+1]
3620+
val = args[i+1]
3621+
if val.lower() in ["no", "off", "false", "0"]:
3622+
config['remote_vnc'] = False
3623+
else:
3624+
config['remote_vnc'] = val
35793625
i += 1
35803626
else:
35813627
config['remote_vnc'] = True
@@ -3608,11 +3654,21 @@ def main():
36083654
i += 1
36093655
else:
36103656
config['synctime'] = True
3657+
else:
3658+
log("Warning: Unrecognized argument: {}".format(arg))
36113659
i += 1
36123660

36133661
if config['debug']:
36143662
debuglog(True, "Debug logging enabled")
36153663

3664+
# If remote VNC not explicitly specified, enable it by default if browser is unavailable
3665+
if config['remote_vnc'] is None:
3666+
if config['vnc'].lower() != "off" and not is_browser_available():
3667+
config['remote_vnc'] = True
3668+
config['remote_vnc_is_default'] = True
3669+
else:
3670+
config['remote_vnc'] = False
3671+
36163672
is_vnc_console = (config.get('vnc') == "console")
36173673

36183674
if not config['os']:
@@ -4539,6 +4595,8 @@ def cmd_exists(cmd):
45394595
cmd_text = format_command_for_display(cmd_list)
45404596
debuglog(config['debug'], "CMD:\n " + cmd_text)
45414597

4598+
vnc_log_path = os.path.join(output_dir, "{}.vncproxy.log".format(vm_name))
4599+
45424600
# Function to start (or restart) the VNC Web Proxy monitoring the given QEMU PID
45434601
def start_vnc_proxy_for_pid(qemu_pid):
45444602
if config['vnc'] != "off" and web_port:
@@ -4553,7 +4611,7 @@ def start_vnc_proxy_for_pid(qemu_pid):
45534611
str(qemu_pid),
45544612
'1' if is_audio_enabled else '0',
45554613
config['qmon'] if config['qmon'] else "",
4556-
os.path.join(output_dir, "{}.vncproxy.log".format(vm_name)),
4614+
vnc_log_path,
45574615
'1' if is_vnc_console else '0',
45584616
'0.0.0.0' if (config['public'] or config['public_vnc']) else '127.0.0.1',
45594617
str(config['remote_vnc']) if config['remote_vnc'] else '0',
@@ -4574,12 +4632,30 @@ def start_vnc_proxy_for_pid(qemu_pid):
45744632
if supports_ansi_color():
45754633
display_local_url = "\x1b[32m{}\x1b[0m".format(local_url)
45764634
log("VNC Web UI available at {}".format(display_local_url))
4635+
4636+
# Start tunnel watcher thread if remote VNC is enabled
4637+
if config.get('remote_vnc'):
4638+
t = threading.Thread(target=watch_vnc_tunnel_log, args=(vnc_log_path, tunnel_wait_stop, config.get('remote_vnc_is_default')))
4639+
t.daemon = True
4640+
t.start()
45774641
return p
45784642
except Exception as e:
45794643
debuglog(config['debug'], "Failed to start VNC proxy process: {}".format(e))
45804644
return None
45814645

45824646
proxy_proc = None
4647+
tunnel_wait_stop = threading.Event()
4648+
4649+
# Pre-startup cleanup of VNC tunnel information
4650+
try:
4651+
if os.path.exists(vnc_log_path):
4652+
os.remove(vnc_log_path)
4653+
remote_file = vnc_log_path.replace(".vncproxy.log", ".remote")
4654+
if os.path.exists(remote_file):
4655+
os.remove(remote_file)
4656+
except:
4657+
pass
4658+
45834659
if config['console']:
45844660
proc = subprocess.Popen(cmd_list)
45854661
proxy_proc = start_vnc_proxy_for_pid(proc.pid)
@@ -4951,6 +5027,7 @@ def finish_wait_timer():
49515027

49525028

49535029
wait_timer_stop.set()
5030+
tunnel_wait_stop.set()
49545031
if wait_timer_thread:
49555032
wait_timer_thread.join(0.2)
49565033
finish_wait_timer()
@@ -4972,7 +5049,7 @@ def finish_wait_timer():
49725049
display_url = tunnel_url
49735050
if supports_ansi_color():
49745051
display_url = "\x1b[32m{}\x1b[0m".format(tunnel_url)
4975-
log("Open this link to access WebVNC (via {}): {}".format(tunnel_service, display_url))
5052+
# Redundant log removed, already handled by watch_vnc_tunnel_log
49765053
else:
49775054
# Check for errors
49785055
err_match = re.search(r"(?:Cloudflare )?Tunnel Error: (.*)", log_text)
@@ -5165,6 +5242,9 @@ def finish_wait_timer():
51655242
if supports_ansi_color():
51665243
display_url = "\x1b[32m{}\x1b[0m".format(tunnel_url)
51675244
log("WebVNC ({}): {}".format(tunnel_service, display_url))
5245+
if config.get('remote_vnc_is_default'):
5246+
log("Notice: Remote VNC tunnel is enabled by default as no local browser was detected.")
5247+
log(" Use '--remote-vnc off' to disable it.")
51685248
log("======================================")
51695249

51705250
if not config['detach']:
@@ -5194,6 +5274,9 @@ def finish_wait_timer():
51945274
if supports_ansi_color():
51955275
display_url = "\x1b[32m{}\x1b[0m".format(tunnel_url)
51965276
log("WebVNC ({}): {}".format(tunnel_service, display_url))
5277+
if config.get('remote_vnc_is_default'):
5278+
log("Notice: Remote VNC tunnel is enabled by default as no local browser was detected.")
5279+
log(" Use '--remote-vnc off' to disable it.")
51975280
log("======================================")
51985281
else:
51995282
log("VM has exited")

0 commit comments

Comments
 (0)