Use selected port for post-install browser watcher

This commit is contained in:
Unsloth 2026-07-19 00:38:43 -07:00
commit b869eabe07
3 changed files with 101 additions and 11 deletions

View file

@ -2688,10 +2688,42 @@ exit 0
# caller explicitly disabled the post-install prompt.
# In non-interactive environments (CI, Docker) just print instructions.
$IsInteractive = (-not $SkipAutostart) -and [Environment]::UserInteractive -and (-not [Console]::IsInputRedirected)
# Select the same bounded free-port range as the generated desktop launcher.
# Passing the selected port to both the server and watcher prevents an
# existing Studio on 8888 from moving the backend while the watcher stays
# behind.
function Find-PostInstallStudioPort {
param([int]$BasePort = 8888, [int]$MaxPortOffset = 20)
$probes = @(
@{ Family = [System.Net.Sockets.AddressFamily]::InterNetwork; Host = '127.0.0.1' },
@{ Family = [System.Net.Sockets.AddressFamily]::InterNetworkV6; Host = '::1' }
)
for ($offset = 0; $offset -le $MaxPortOffset; $offset++) {
$candidate = $BasePort + $offset
$busy = $false
foreach ($probe in $probes) {
$client = $null
try {
$client = [System.Net.Sockets.TcpClient]::new($probe.Family)
$connect = $client.ConnectAsync($probe.Host, $candidate)
if ($connect.Wait(100) -and $client.Connected) {
$busy = $true
break
}
} catch {
} finally {
if ($client) { $client.Dispose() }
}
}
if (-not $busy) { return $candidate }
}
return $BasePort
}
# Background watcher for the foreground launch below: once the server is
# healthy, open the browser per the persisted preference (mirrors the
# desktop launcher). Guarded by the per-install root id so a different
# Studio already on the port is never the one opened.
# healthy on the selected port, open the browser per the persisted
# preference. Guarded by the per-install root id so a different Studio is
# never the one opened.
$_browserWatch = {
param($RootId, $Port)
$deadline = (Get-Date).AddSeconds(120)
@ -2711,6 +2743,7 @@ exit 0
Write-Host ""
$reply = Read-Host " Start Unsloth Studio now? [Y/n]"
if ([string]::IsNullOrWhiteSpace($reply) -or $reply -match '^[Yy]') {
$_launchPort = Find-PostInstallStudioPort
# Open the browser once the server is up, unless opted out. The
# server prints its own URL, so no watcher is needed when off.
if ($OpenBrowserPref -ne '0') {
@ -2720,10 +2753,10 @@ exit 0
try { $_watchRootId = ([System.IO.File]::ReadAllText($_watchIdFile)).Trim() } catch {}
}
try {
$null = Start-Job -ScriptBlock $_browserWatch -ArgumentList @($_watchRootId, 8888)
$null = Start-Job -ScriptBlock $_browserWatch -ArgumentList @($_watchRootId, $_launchPort)
} catch {}
}
& $UnslothExe studio -p 8888
& $UnslothExe studio -p $_launchPort
} else {
step "launch" "to start later, run:"
substep "unsloth studio -p 8888"

View file

@ -3295,10 +3295,48 @@ printf " ${C_TITLE}%s${C_RST}\n" "Unsloth Studio installed!"
printf " ${C_DIM}%s${C_RST}\n" "$RULE"
echo ""
# Select the same bounded free-port range as the generated desktop launcher.
# Passing the selected port to both the server and watcher prevents an existing
# Studio on 8888 from making the backend move while the watcher stays behind.
_find_post_install_port() {
_pifp_base="${1:-8888}"
_pifp_max_offset="${2:-20}"
"$VENV_DIR/bin/python" - "$_pifp_base" "$_pifp_max_offset" <<'PY'
import socket
import sys
base = int(sys.argv[1])
max_offset = int(sys.argv[2])
def is_free(port):
endpoints = (
(socket.AF_INET, ("127.0.0.1", port)),
(socket.AF_INET6, ("::1", port, 0, 0)),
)
for family, address in endpoints:
try:
with socket.socket(family, socket.SOCK_STREAM) as probe:
probe.settimeout(0.1)
if probe.connect_ex(address) == 0:
return False
except OSError:
# IPv6 can be unavailable; match the backend's loopback probe.
continue
return True
for offset in range(max_offset + 1):
candidate = base + offset
if is_free(candidate):
print(candidate)
raise SystemExit(0)
raise SystemExit(1)
PY
}
# Background watcher for the post-install foreground launch below: once the
# server is healthy, open the browser per the persisted preference (mirrors
# the desktop launcher). Guarded by the per-install root id so a different
# Studio already on the port is never the one opened.
# server is healthy on the selected port, open the browser per the persisted
# preference. Guarded by the per-install root id so a different Studio is never
# the one opened.
_post_install_browser_watch() {
_pibw_port="$1"
_pibw_url="http://localhost:$_pibw_port"
@ -3353,10 +3391,14 @@ if [ "$_SKIP_AUTOSTART" != true ] && [ -t 1 ]; then
case "${_reply:-y}" in
[Yy]*|"")
step "launch" "starting Unsloth Studio..."
_post_install_port=$(_find_post_install_port 8888 20) || _post_install_port=8888
case "$_post_install_port" in
''|*[!0-9]*) _post_install_port=8888 ;;
esac
# Open the browser once the server is up, unless opted out. The
# server prints its own URL, so no watcher is needed when off.
if [ "${_STUDIO_OPEN_BROWSER:-1}" != "0" ]; then
_post_install_browser_watch 8888
_post_install_browser_watch "$_post_install_port"
fi
# Detach stdin from the `curl | sh` pipe: as a foreground server the
# studio would otherwise drain the rest of this piped script, leaving
@ -3366,7 +3408,7 @@ if [ "$_SKIP_AUTOSTART" != true ] && [ -t 1 ]; then
trap '' INT
# `|| ...`: capture the exit code without set -e aborting first.
_LAUNCH_EXIT=0
(trap - INT; exec "$VENV_DIR/bin/unsloth" studio -p 8888 </dev/null) || _LAUNCH_EXIT=$?
(trap - INT; exec "$VENV_DIR/bin/unsloth" studio -p "$_post_install_port" </dev/null) || _LAUNCH_EXIT=$?
if [ "$_LAUNCH_EXIT" -ne 0 ] && [ "$_MIGRATED" = true ]; then
echo ""
echo "⚠️ Unsloth Studio failed to start after migration."

View file

@ -94,7 +94,13 @@ assert_contains \
# The post-install foreground launch honors the preference too.
assert_contains \
"install.sh: post-install launch opens browser via gated watcher" \
"$_installer" "_post_install_browser_watch 8888"
"$_installer" '_post_install_browser_watch "$_post_install_port"'
assert_contains \
"install.sh: post-install launch selects a free port" \
"$_installer" "_find_post_install_port()"
assert_contains \
"install.sh: server uses the watcher-selected port" \
"$_installer" 'studio -p "$_post_install_port"'
# The --shortcuts-only early exit must not EPIPE a curl | sh pipeline.
assert_contains \
"install.sh: shortcuts-only exit drains piped stdin" \
@ -182,6 +188,15 @@ assert_file_contains \
assert_file_contains \
"install.ps1: post-install launch opens browser via gated watcher" \
"$INSTALL_PS1" 'Start-Job -ScriptBlock $_browserWatch'
assert_file_contains \
"install.ps1: post-install launch selects a free port" \
"$INSTALL_PS1" 'function Find-PostInstallStudioPort {'
assert_file_contains \
"install.ps1: watcher receives the selected port" \
"$INSTALL_PS1" 'ArgumentList @($_watchRootId, $_launchPort)'
assert_file_contains \
"install.ps1: server uses the watcher-selected port" \
"$INSTALL_PS1" '& $UnslothExe studio -p $_launchPort'
# All launcher URL opens must route through the gated helper. The one
# allowed direct call is inside the post-install $_browserWatch scriptblock,
# whose Start-Job call site is itself gated on the preference.