Add no-browser launch option for the Studio desktop launcher
The generated launchers (launch-studio.sh / launch-studio.ps1) always opened the default browser once the server became healthy. Add a --no-browser launcher flag, the UNSLOTH_STUDIO_NO_BROWSER env var, and a persisted installer preference (studio.conf / baked into the ps1 launcher) with an interactive install prompt. When auto-open is off the launcher still starts or attaches to the server and prints the URL, for users who run Studio as a browser PWA or app window. The --shortcuts-only refresh run by studio update preserves the choice.
This commit is contained in:
parent
3b73cd8829
commit
c980658592
4 changed files with 277 additions and 3 deletions
|
|
@ -86,6 +86,8 @@ unsloth studio -p 8888
|
|||
```
|
||||
For cloud or global access, add `-H 0.0.0.0`. By default, Unsloth is accessible only locally.
|
||||
|
||||
Launching from the terminal never opens a browser -- open the printed URL yourself. The desktop shortcut opens your default browser once the server is up; to launch the server without that (e.g. when running Studio as a browser PWA / app window), answer "n" at the installer's browser prompt, pass `--no-browser` to the launcher, or set `UNSLOTH_STUDIO_NO_BROWSER=1`.
|
||||
|
||||
To reach Studio over HTTPS, use `unsloth studio --secure`. Studio stays bound to localhost and is reached only through a free Cloudflare tunnel, which publishes it at a public `https://*.trycloudflare.com` URL (it fails closed if the tunnel can't start, so the raw port is never exposed). This makes Studio reachable from the internet, so anyone with the link and API key can use it and run code: keep your API key private (see Remote access below).
|
||||
|
||||
#### Docker
|
||||
|
|
|
|||
58
install.ps1
58
install.ps1
|
|
@ -99,6 +99,8 @@ function Install-UnslothStudio {
|
|||
$TauriMode = $false
|
||||
$SkipTorch = $false
|
||||
$ShortcutsOnly = $false
|
||||
# Launcher browser auto-open: "" = undecided (prompt, else keep existing, else on).
|
||||
$OpenBrowserPref = ""
|
||||
$WithLlamaCppDir = ""
|
||||
$argList = $args
|
||||
for ($i = 0; $i -lt $argList.Count; $i++) {
|
||||
|
|
@ -109,6 +111,8 @@ function Install-UnslothStudio {
|
|||
"--verbose" { $script:UnslothVerbose = $true }
|
||||
"-v" { $script:UnslothVerbose = $true }
|
||||
"--shortcuts-only" { $ShortcutsOnly = $true }
|
||||
"--no-browser" { $OpenBrowserPref = '0' }
|
||||
"--browser" { $OpenBrowserPref = '1' }
|
||||
"--package" {
|
||||
$i++
|
||||
if ($i -ge $argList.Count) {
|
||||
|
|
@ -640,6 +644,20 @@ function Install-UnslothStudio {
|
|||
"`$portFile = `$null`n`$mutexName = 'Local\UnslothStudioLauncher'`n"
|
||||
}
|
||||
|
||||
# Browser auto-open: explicit installer choice wins; else keep the
|
||||
# value baked into the existing launcher so `studio update`
|
||||
# (--shortcuts-only) never resets it.
|
||||
$_openBrowser = $OpenBrowserPref
|
||||
if (-not $_openBrowser -and (Test-Path -LiteralPath $launcherPs1)) {
|
||||
try {
|
||||
$_prevLauncher = [System.IO.File]::ReadAllText($launcherPs1)
|
||||
if ($_prevLauncher -match "(?m)^\`$openBrowserDefault = '([01])'") {
|
||||
$_openBrowser = $Matches[1]
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
if ($_openBrowser -ne '0') { $_openBrowser = '1' }
|
||||
|
||||
$launcherContent = @"
|
||||
$studioHomeExport`$ErrorActionPreference = 'Stop'
|
||||
`$basePort = 8888
|
||||
|
|
@ -647,6 +665,30 @@ $studioHomeExport`$ErrorActionPreference = 'Stop'
|
|||
`$timeoutSec = 60
|
||||
`$pollIntervalMs = 1000
|
||||
`$_ExpectedStudioRootId = '$_studioRootId'
|
||||
`$openBrowserDefault = '$_openBrowser'
|
||||
|
||||
# Browser auto-open: disabled by -NoBrowser/--no-browser, the
|
||||
# UNSLOTH_STUDIO_NO_BROWSER env var, or the baked installer preference.
|
||||
# When off the server still starts; the URL is printed instead (PWA use).
|
||||
`$openBrowser = (`$openBrowserDefault -ne '0')
|
||||
if (`$env:UNSLOTH_STUDIO_NO_BROWSER -and
|
||||
(`$env:UNSLOTH_STUDIO_NO_BROWSER -notin @('0', 'false', 'no', 'off'))) {
|
||||
`$openBrowser = `$false
|
||||
}
|
||||
foreach (`$_launchArg in `$args) {
|
||||
if (`$_launchArg -in @('-NoBrowser', '--no-browser')) { `$openBrowser = `$false }
|
||||
elseif (`$_launchArg -in @('-Browser', '--browser')) { `$openBrowser = `$true }
|
||||
}
|
||||
|
||||
function Open-StudioUrl {
|
||||
param([Parameter(Mandatory = `$true)][string]`$Url)
|
||||
if (`$openBrowser) {
|
||||
Start-Process `$Url
|
||||
} else {
|
||||
# Hidden-window launches have no console; never fail on the echo.
|
||||
try { Write-Host "Unsloth Studio is running at: `$Url" } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
function Test-StudioHealth {
|
||||
param([Parameter(Mandatory = `$true)][int]`$Port)
|
||||
|
|
@ -743,7 +785,7 @@ function Find-FreeLaunchPort {
|
|||
# If Studio is already healthy on any expected port, just open it and exit.
|
||||
`$existingPort = Find-HealthyStudioPort
|
||||
if (`$existingPort) {
|
||||
Start-Process "http://localhost:`$existingPort"
|
||||
Open-StudioUrl "http://localhost:`$existingPort"
|
||||
exit 0
|
||||
}
|
||||
|
||||
|
|
@ -760,7 +802,7 @@ try {
|
|||
`$deadline = (Get-Date).AddSeconds(`$timeoutSec)
|
||||
while ((Get-Date) -lt `$deadline) {
|
||||
`$port = Find-HealthyStudioPort
|
||||
if (`$port) { Start-Process "http://localhost:`$port"; exit 0 }
|
||||
if (`$port) { Open-StudioUrl "http://localhost:`$port"; exit 0 }
|
||||
Start-Sleep -Milliseconds `$pollIntervalMs
|
||||
}
|
||||
exit 0
|
||||
|
|
@ -809,7 +851,7 @@ try {
|
|||
[System.IO.File]::WriteAllText(`$portFile, "`$launchPort`n")
|
||||
} catch {}
|
||||
}
|
||||
Start-Process "http://localhost:`$launchPort"
|
||||
Open-StudioUrl "http://localhost:`$launchPort"
|
||||
`$browserOpened = `$true
|
||||
break
|
||||
}
|
||||
|
|
@ -2570,6 +2612,16 @@ exit 0
|
|||
return
|
||||
}
|
||||
|
||||
# Ask once (interactive installs only) whether the launcher should open
|
||||
# the browser after the server is up. Skipped when --no-browser/--browser
|
||||
# was passed or input is redirected; then an existing choice is kept.
|
||||
$_browserPromptOk = [Environment]::UserInteractive -and (-not [Console]::IsInputRedirected)
|
||||
if (-not $OpenBrowserPref -and $_browserPromptOk) {
|
||||
Write-Host ""
|
||||
$_browserReply = Read-Host " Open Unsloth Studio in your default browser after launch? [Y/n]"
|
||||
$OpenBrowserPref = if ($_browserReply -match '^[Nn]') { '0' } else { '1' }
|
||||
}
|
||||
|
||||
# New-StudioShortcuts gates the .lnk shortcuts on env-mode internally.
|
||||
New-StudioShortcuts -UnslothExePath $UnslothExe
|
||||
|
||||
|
|
|
|||
46
install.sh
46
install.sh
|
|
@ -12,6 +12,7 @@
|
|||
# curl -fsSL https://unsloth.ai/install.sh | UNSLOTH_PYTHON=3.12 sh # pin Python version
|
||||
# curl -fsSL https://unsloth.ai/install.sh | UNSLOTH_STUDIO_HOME=/abs/path sh
|
||||
# Equivalent flags: ./install.sh --no-torch --python 3.12 (or pipe them: sh -s -- --no-torch)
|
||||
# ./install.sh --no-browser: launcher starts the server without opening the browser.
|
||||
#
|
||||
# Install dir priority: UNSLOTH_STUDIO_HOME > STUDIO_HOME (alias) > $HOME/.unsloth/studio
|
||||
#
|
||||
|
|
@ -51,6 +52,8 @@ _USER_PYTHON=""
|
|||
_NO_TORCH_FLAG=false
|
||||
_VERBOSE=false
|
||||
_SHORTCUTS_ONLY=false
|
||||
# Launcher browser auto-open: "" = undecided (prompt, else keep existing, else on).
|
||||
_STUDIO_OPEN_BROWSER=""
|
||||
_next_is_package=false
|
||||
_next_is_python=false
|
||||
_next_is_llama_cpp_dir=false
|
||||
|
|
@ -82,6 +85,8 @@ for arg in "$@"; do
|
|||
--no-torch) _NO_TORCH_FLAG=true ;;
|
||||
--verbose|-v) _VERBOSE=true ;;
|
||||
--shortcuts-only) _SHORTCUTS_ONLY=true ;;
|
||||
--no-browser) _STUDIO_OPEN_BROWSER=0 ;;
|
||||
--browser) _STUDIO_OPEN_BROWSER=1 ;;
|
||||
--with-llama-cpp-dir) _next_is_llama_cpp_dir=true ;;
|
||||
esac
|
||||
done
|
||||
|
|
@ -647,6 +652,21 @@ if [ -z "${UNSLOTH_EXE:-}" ] || [ ! -x "${UNSLOTH_EXE:-}" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
# Browser auto-open. Priority: --no-browser/--browser arg, then
|
||||
# UNSLOTH_STUDIO_NO_BROWSER env var, then studio.conf, default on.
|
||||
# When off the server still starts; the URL is printed instead (PWA use).
|
||||
OPEN_BROWSER="${STUDIO_OPEN_BROWSER:-1}"
|
||||
case "${UNSLOTH_STUDIO_NO_BROWSER:-}" in
|
||||
''|0|false|FALSE|no|NO|off|OFF) ;;
|
||||
*) OPEN_BROWSER=0 ;;
|
||||
esac
|
||||
for _arg in "$@"; do
|
||||
case "$_arg" in
|
||||
--no-browser) OPEN_BROWSER=0 ;;
|
||||
--browser) OPEN_BROWSER=1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
BASE_PORT=8888
|
||||
MAX_PORT_OFFSET=20
|
||||
TIMEOUT_SEC=60
|
||||
|
|
@ -780,6 +800,10 @@ _find_launch_port() {
|
|||
# ── Open browser ──
|
||||
_open_browser() {
|
||||
_url="$1"
|
||||
if [ "$OPEN_BROWSER" = "0" ]; then
|
||||
echo "Unsloth Studio is running at: $_url"
|
||||
return 0
|
||||
fi
|
||||
if [ "$(uname)" = "Darwin" ] && command -v open >/dev/null 2>&1; then
|
||||
open "$_url"
|
||||
elif grep -qi microsoft /proc/version 2>/dev/null; then
|
||||
|
|
@ -1011,11 +1035,21 @@ LAUNCHER_EOF
|
|||
|
||||
chmod +x "$_css_launcher"
|
||||
|
||||
# Browser auto-open: explicit installer choice wins; else keep the existing
|
||||
# studio.conf value so `studio update` (--shortcuts-only) never resets it.
|
||||
_css_open_browser="${_STUDIO_OPEN_BROWSER:-}"
|
||||
if [ -z "$_css_open_browser" ] && [ -f "$_css_data_dir/studio.conf" ]; then
|
||||
_css_open_browser=$(sed -n "s/^STUDIO_OPEN_BROWSER='\([01]\)'\$/\1/p" \
|
||||
"$_css_data_dir/studio.conf" 2>/dev/null | head -n 1)
|
||||
fi
|
||||
[ "$_css_open_browser" = "0" ] || _css_open_browser=1
|
||||
|
||||
# studio.conf: exe path + (env-mode only) persisted env vars so fresh
|
||||
# shells launch the right install without re-exporting.
|
||||
_css_quoted_exe=$(printf '%s' "$_css_exe" | sed "s/'/'\\\\''/g")
|
||||
{
|
||||
printf '%s\n' "UNSLOTH_EXE='$_css_quoted_exe'"
|
||||
printf '%s\n' "STUDIO_OPEN_BROWSER='$_css_open_browser'"
|
||||
if [ "$_STUDIO_HOME_REDIRECT" = "env" ]; then
|
||||
# When an override resolves to the legacy default, llama.cpp
|
||||
# still lives at ~/.unsloth/llama.cpp (one shared build).
|
||||
|
|
@ -3160,6 +3194,18 @@ esac
|
|||
# create_studio_shortcuts gates persistent menu shortcuts on env-mode;
|
||||
# launcher + studio.conf + icon are always written.
|
||||
if [ "$TAURI_MODE" != true ]; then
|
||||
# Ask once (interactive installs only) whether the launcher should open
|
||||
# the browser after the server is up. Skipped when --no-browser/--browser
|
||||
# was passed or no TTY; then an existing choice is kept, defaulting to on.
|
||||
if [ -z "$_STUDIO_OPEN_BROWSER" ] && [ -t 1 ] && [ -r /dev/tty ]; then
|
||||
echo ""
|
||||
printf " Open Unsloth Studio in your default browser after launch? [Y/n] "
|
||||
read -r _browser_reply </dev/tty || _browser_reply="y"
|
||||
case "$_browser_reply" in
|
||||
[nN]*) _STUDIO_OPEN_BROWSER=0 ;;
|
||||
*) _STUDIO_OPEN_BROWSER=1 ;;
|
||||
esac
|
||||
fi
|
||||
create_studio_shortcuts "$VENV_ABS_BIN/unsloth" "$OS"
|
||||
fi
|
||||
|
||||
|
|
|
|||
174
tests/sh/test_launcher_no_browser.sh
Executable file
174
tests/sh/test_launcher_no_browser.sh
Executable file
|
|
@ -0,0 +1,174 @@
|
|||
#!/bin/bash
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
# The generated desktop launchers must support suppressing the automatic
|
||||
# default-browser open after the server becomes healthy (PWA users run Studio
|
||||
# in a browser-app window, not the OS default browser). Covered surface:
|
||||
# - launch-studio.sh: --no-browser flag, UNSLOTH_STUDIO_NO_BROWSER env var,
|
||||
# persisted STUDIO_OPEN_BROWSER from studio.conf; prints the URL when off.
|
||||
# - install.sh: --no-browser flag, interactive prompt, studio.conf persistence
|
||||
# that survives the --shortcuts-only refresh run by `unsloth studio update`.
|
||||
# - install.ps1: same feature baked into launch-studio.ps1 (Open-StudioUrl).
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
INSTALL_SH="$SCRIPT_DIR/../../install.sh"
|
||||
INSTALL_PS1="$SCRIPT_DIR/../../install.ps1"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
assert_contains() {
|
||||
_label="$1"; _haystack="$2"; _needle="$3"
|
||||
if echo "$_haystack" | grep -qF -- "$_needle"; then
|
||||
echo " PASS: $_label"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
echo " FAIL: $_label (expected to find '$_needle')"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
assert_not_contains() {
|
||||
_label="$1"; _haystack="$2"; _needle="$3"
|
||||
if echo "$_haystack" | grep -qF -- "$_needle"; then
|
||||
echo " FAIL: $_label (found '$_needle' but should not)"
|
||||
FAIL=$((FAIL + 1))
|
||||
else
|
||||
echo " PASS: $_label"
|
||||
PASS=$((PASS + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "=== install.sh launcher template ==="
|
||||
|
||||
# Extract the heredoc that generates ~/.local/share/unsloth/launch-studio.sh.
|
||||
_launcher=$(awk '/cat > "\$_css_launcher"/{found=1} found{print} /^LAUNCHER_EOF$/{found=0}' "$INSTALL_SH")
|
||||
assert_contains \
|
||||
"launcher template: --no-browser argument handled" \
|
||||
"$_launcher" "--no-browser) OPEN_BROWSER=0"
|
||||
assert_contains \
|
||||
"launcher template: UNSLOTH_STUDIO_NO_BROWSER env var handled" \
|
||||
"$_launcher" "UNSLOTH_STUDIO_NO_BROWSER"
|
||||
assert_contains \
|
||||
"launcher template: studio.conf preference is the default" \
|
||||
"$_launcher" 'OPEN_BROWSER="${STUDIO_OPEN_BROWSER:-1}"'
|
||||
assert_contains \
|
||||
"launcher template: _open_browser is gated on OPEN_BROWSER" \
|
||||
"$_launcher" '[ "$OPEN_BROWSER" = "0" ]'
|
||||
assert_contains \
|
||||
"launcher template: URL still printed when auto-open is off" \
|
||||
"$_launcher" "Unsloth Studio is running at:"
|
||||
|
||||
echo ""
|
||||
echo "=== install.sh installer plumbing ==="
|
||||
|
||||
_installer=$(cat "$INSTALL_SH")
|
||||
assert_contains \
|
||||
"install.sh: --no-browser flag parsed" \
|
||||
"$_installer" "--no-browser) _STUDIO_OPEN_BROWSER=0"
|
||||
assert_contains \
|
||||
"install.sh: preference persisted into studio.conf" \
|
||||
"$_installer" "STUDIO_OPEN_BROWSER='\$_css_open_browser'"
|
||||
assert_contains \
|
||||
"install.sh: existing studio.conf choice preserved on refresh" \
|
||||
"$_installer" "s/^STUDIO_OPEN_BROWSER="
|
||||
assert_contains \
|
||||
"install.sh: interactive prompt asks about browser auto-open" \
|
||||
"$_installer" "Open Unsloth Studio in your default browser after launch?"
|
||||
|
||||
echo ""
|
||||
echo "=== install.sh _open_browser gating (functional) ==="
|
||||
|
||||
# Extract the _open_browser function from the (column-0) launcher heredoc and
|
||||
# drive it with stubbed browser openers on PATH.
|
||||
_fn=$(printf '%s\n' "$_launcher" | awk '/^_open_browser\(\) \{/{found=1} found{print} found && /^\}/{exit}')
|
||||
if [ -z "$_fn" ]; then
|
||||
echo " FAIL: could not extract _open_browser from launcher template"
|
||||
FAIL=$((FAIL + 1))
|
||||
else
|
||||
_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$_tmp"' EXIT
|
||||
for _stub in open xdg-open; do
|
||||
printf '#!/bin/sh\necho "BROWSER_OPENED:$1" >> "$RECORD"\n' > "$_tmp/$_stub"
|
||||
chmod +x "$_tmp/$_stub"
|
||||
done
|
||||
|
||||
# Off: no browser process, URL echoed instead.
|
||||
_out=$(RECORD="$_tmp/record_off" PATH="$_tmp:$PATH" bash -c \
|
||||
"OPEN_BROWSER=0; $_fn; _open_browser http://localhost:9999")
|
||||
assert_contains \
|
||||
"OPEN_BROWSER=0 prints the URL" \
|
||||
"$_out" "Unsloth Studio is running at: http://localhost:9999"
|
||||
if [ -f "$_tmp/record_off" ]; then
|
||||
echo " FAIL: OPEN_BROWSER=0 still invoked a browser opener"
|
||||
FAIL=$((FAIL + 1))
|
||||
else
|
||||
echo " PASS: OPEN_BROWSER=0 does not invoke a browser opener"
|
||||
PASS=$((PASS + 1))
|
||||
fi
|
||||
|
||||
# On (default): browser opener invoked with the URL.
|
||||
RECORD="$_tmp/record_on" PATH="$_tmp:$PATH" bash -c \
|
||||
"OPEN_BROWSER=1; $_fn; _open_browser http://localhost:9999" > /dev/null
|
||||
# xdg-open is backgrounded inside _open_browser; give the stub a moment.
|
||||
_i=0
|
||||
while [ ! -s "$_tmp/record_on" ] && [ "$_i" -lt 20 ]; do
|
||||
sleep 0.1
|
||||
_i=$((_i + 1))
|
||||
done
|
||||
assert_contains \
|
||||
"OPEN_BROWSER=1 invokes a browser opener with the URL" \
|
||||
"$(cat "$_tmp/record_on" 2>/dev/null)" "BROWSER_OPENED:http://localhost:9999"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== install.ps1 launcher template ==="
|
||||
|
||||
# grep the file directly: piping the whole installer into grep -q trips
|
||||
# SIGPIPE noise from echo once grep exits on first match.
|
||||
assert_file_contains() {
|
||||
_label="$1"; _file="$2"; _needle="$3"
|
||||
if grep -qF -- "$_needle" "$_file"; then
|
||||
echo " PASS: $_label"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
echo " FAIL: $_label (expected to find '$_needle')"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
assert_file_contains \
|
||||
"install.ps1: --no-browser flag parsed" \
|
||||
"$INSTALL_PS1" "\"--no-browser\" { \$OpenBrowserPref = '0' }"
|
||||
assert_file_contains \
|
||||
"install.ps1: preference baked into launch-studio.ps1" \
|
||||
"$INSTALL_PS1" "openBrowserDefault = '\$_openBrowser'"
|
||||
assert_file_contains \
|
||||
"install.ps1: launcher honors UNSLOTH_STUDIO_NO_BROWSER" \
|
||||
"$INSTALL_PS1" "UNSLOTH_STUDIO_NO_BROWSER"
|
||||
assert_file_contains \
|
||||
"install.ps1: gated helper defined" \
|
||||
"$INSTALL_PS1" "function Open-StudioUrl {"
|
||||
assert_file_contains \
|
||||
"install.ps1: interactive prompt asks about browser auto-open" \
|
||||
"$INSTALL_PS1" "Open Unsloth Studio in your default browser after launch?"
|
||||
# All launcher URL opens must route through the gated helper.
|
||||
_ps1_direct_open=$(grep -cF 'Start-Process "http://localhost:' "$INSTALL_PS1" || true)
|
||||
if [ "$_ps1_direct_open" -eq 0 ]; then
|
||||
echo " PASS: no ungated Start-Process http://localhost calls remain"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
echo " FAIL: $_ps1_direct_open ungated Start-Process http://localhost call(s) remain"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Results ==="
|
||||
echo " PASS: $PASS"
|
||||
echo " FAIL: $FAIL"
|
||||
if [ "$FAIL" -gt 0 ]; then
|
||||
echo "FAILED"
|
||||
exit 1
|
||||
fi
|
||||
echo "ALL PASSED"
|
||||
Loading…
Add table
Add a link
Reference in a new issue