1
0
mirror of https://github.com/ohmyzsh/ohmyzsh.git synced 2026-02-11 05:39:45 +08:00
This commit is contained in:
Marc Cornellà 2026-02-08 20:39:04 -06:00 committed by GitHub
commit bfbc88b86a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 386 additions and 7 deletions

View File

@ -0,0 +1,155 @@
#autoload
# This function serves a specified file over HTTP 1.0 on localhost, at a random
# available port. It waits for a single GET request to the correct URL and then
# responds with the file contents. If any other request is received, it responds
# with a 404 Not Found. The server automatically shuts down after serving the
# file or after a specified timeout.
#
# Usage:
# serve-file-and-quit <file-to-serve> [timeout-secs] [Access-Control-Allow-Origin]
# <file-to-serve> Path to the file to be served (required)
# [timeout-secs] Number of seconds to wait for a connection before quitting (default: 30, max: 60)
# [Access-Control-Allow-Origin] Value for the Access-Control-Allow-Origin header (default: "null")
#
# If called from a subshell, it outputs the URL where the file can be accessed, and
# then closes stdout to avoid hanging while the server is listening. The caller can
# capture this output.
#
# It is intended for temporary sharing of files between local applications,
# such as opening a trace file in a web-based viewer (trace.ohmyz.sh). It is not
# meant for production use or serving files over the internet.
#
# It's a minimal implementation using zsh's built-in TCP capabilities, primarily
# to avoid external dependencies.
#
# Security considerations:
#
# - This server listens on 0.0.0.0 (all interfaces), so it may be accessible from
# other devices on the same network. I haven't found a way to bind only to localhost
# using zsh's ztcp module.
# - As the port is randomized, and the server is short-lived, the risk is mitigated,
# but not eliminated. An additional mitigation could be to use a random URL path segment.
# - It's not a proper HTTP server, so it uses very rudimentary request parsing and
# response generation. It currently does not support binary files, and probably
# never will.
setopt localoptions localtraps
zmodload zsh/net/tcp
zmodload zsh/zselect
local file="$1"
local timeout_secs="${2:-30}"
local AccessControlAllowOrigin=${3:-"null"}
if [[ -z "$file" ]]; then
print -ru2 "Usage: $0 <file-to-serve> [timeout-secs] [Access-Control-Allow-Origin]"
return 1
fi
if [[ ! -r "$file" ]]; then
print -ru2 "Error: file '$file' not found or not readable."
return 1
fi
if (( timeout_secs < 1 || timeout_secs > 60 )); then
print -ru2 "Error: timeout must be a positive integer between 1 and 60."
return 1
fi
local pathname="$(omz_urlencode -P "/${file:t}")"
# 1. Get a random available port
local port=$(( RANDOM % 32767 + 1100 ))
while ! ztcp -l "$port" &>/dev/null; do
(( port++ ))
if (( port > 65535 )); then
print -ru2 "Error: failed to start server: no available ports."
return 1
fi
done
ztcp -c $REPLY # inmediately close the test connection
# 2. Output file URL to stdout for caller to use
# Here the subcommand substitution captures the output and returns it to the caller
if [[ $ZSH_SUBSHELL -gt 0 ]]; then
echo "http://localhost:$port${pathname}"
exec >/dev/null # close stdout in the subshell to avoid hanging
fi
# Start of asynchronous server block
{
local response_body request_line listen_fd fd
# 1. Start listening on the selected port
if ! ztcp -l $port; then
print -ru2 "Error: failed to start server on port $port."
return 1
fi
listen_fd=$REPLY
print -ru2 "Serving file '${file:t}' on http://localhost:$port${pathname} ..."
# 2. Wait for a connection, with a timeout (in hundredths of a second)
while zselect -t $(( timeout_secs * 100 )) -r $listen_fd; do
# 3. Accept the connection
ztcp -a $listen_fd
fd=$REPLY
# 4. Read the request line (the first line is usually enough to determine the path)
# Note: read up to 2048 bytes to avoid blocking indefinitely
sysread -s 2048 -i $fd request
local request_line="${${(f)request}[1]}"
# 5. If the request is not for the expected pathname, respond with 404 Not Found
if [[ "${${(f)request}[1]}" != "GET ${pathname} HTTP/1."* ]]; then
local response_body="404 Not Found"
local http_response=$'HTTP/1.0 404 Not Found\r
Content-Type: text/plain\r
Content-Length: %d\r
\r
%s\r
'
>&$fd builtin printf -- "$http_response" \
$(( ${#response_body} + 2 )) \
"$response_body"
# 5.1. Close the connection file descriptor and try again
ztcp -c $fd
continue
fi
# 6. If the request matches the pathname, respond with the contents of the file
local response_body="$(<$file)"
local http_response=$'HTTP/1.0 200 OK\r
Access-Control-Allow-Origin: %s\r
Content-Type: text/plain;charset=utf-8\r
Content-Length: %d\r
\r
%s\r
'
>&$fd builtin printf -- "$http_response" \
"$AccessControlAllowOrigin" \
$(( ${#response_body} + 2 )) \
"$response_body"
# 6.1. Close the connection file descriptor and stop the loop
ztcp -c $fd
break
done
return 0
} always {
# Non-zero if error or timeout occurred
local ret=$?
# 7. Close remaining file descriptors and shut down the server
print -ru2 "Server stopped."
[[ -z "$fd" ]] || ztcp -c $fd 2>/dev/null
[[ -z "$listen_fd" ]] || ztcp -c $listen_fd 2>/dev/null
return $ret
} &|

58
hooks/zshenv Normal file
View File

@ -0,0 +1,58 @@
omzp:start() {
setopt localoptions localtraps
# initialization
zmodload zsh/datetime
typeset -Ag __OMZP
__OMZP=(
PS4 "$PS4"
start "$EPOCHREALTIME"
outfile "${1:-$HOME}/${${SHELL:t}#-}.$EPOCHSECONDS.$$.zsh-trace.log"
)
typeset -g PS4="+Z|%e|%D{%s.%9.}|%N|%x|%I> "
# unload profiler on startup end
autoload -Uz add-zsh-hook
add-zsh-hook precmd omzp:stop
# redirect debug output to profiler log file
exec 3>&2 2>${__OMZP[outfile]}
# enable zsh debug mode
trap 'setopt xtrace noevallineno' EXIT
}
# Force this function to be executed in noxtrace mode
emulate zsh +x -c '
omzp:stop() {
setopt localoptions localtraps
trap "{ setopt noxtrace evallineno } 2>/dev/null; exec 2>&3 3>&-" EXIT
# restore PS4
typeset -g PS4="$__OMZP[PS4]"
unset "__OMZP[PS4]"
# remove precmd function
add-zsh-hook -d precmd omzp:stop
unfunction omzp:stop
local startup=$(( (${(%):-"%D{%s.%9.}"} - __OMZP[start]) * 1e3 ))
printf "%.3f ms %s \n" "$startup" "${__OMZP[outfile]:t}"
}'
# TODO: this is duplicated from init script, fix later
# Init $ZSH path
[[ -n "$ZSH" ]] || export ZSH="${${(%):-%x}:a:h:h}"
# Set ZSH_CACHE_DIR to the path where cache files should be created
# or else we will use the default cache/
[[ -n "$ZSH_CACHE_DIR" ]] || ZSH_CACHE_DIR="$ZSH/cache"
# Make sure $ZSH_CACHE_DIR is writable, otherwise use a directory in $HOME
if [[ ! -w "$ZSH_CACHE_DIR" ]]; then
ZSH_CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/oh-my-zsh"
fi
# Set OMZ_TRACES directory
OMZ_TRACES="${OMZ_TRACES:-"$ZSH_CACHE_DIR/.traces"}"
! 'builtin' 'test' -f "${OMZ_TRACES}/.enabled" || omzp:start "$OMZ_TRACES"

View File

@ -30,6 +30,7 @@ function _omz {
'reload:Reload the current zsh session'
'shop:Open the Oh My Zsh shop'
'theme:Manage themes'
'trace:Manage debug tracing'
'update:Update Oh My Zsh'
'version:Show the version'
)
@ -53,6 +54,15 @@ function _omz {
_describe 'command' subcmds ;;
theme) subcmds=('list:List themes' 'set:Set a theme in your .zshrc file' 'use:Load a theme')
_describe 'command' subcmds ;;
trace) subcmds=(
'clean:Delete all traces'
'list:List traces'
'off:Turn debug tracing off'
'on:Turn debug tracing on'
'toggle:Toggle debug tracing'
'view:View trace in browser'
)
_describe 'command' subcmds ;;
esac
elif (( CURRENT == 4 )); then
case "${words[2]}::${words[3]}" in
@ -81,6 +91,12 @@ function _omz {
local -aU themes
themes=("$ZSH"/themes/*.zsh-theme(-.N:t:r) "$ZSH_CUSTOM"/**/*.zsh-theme(-.N:r:gs:"$ZSH_CUSTOM"/themes/:::gs:"$ZSH_CUSTOM"/:::))
_describe 'theme' themes ;;
trace::view)
local -a opts traces
traces=("${ZSH_CACHE_DIR}/.traces/"*.log(N:t))
# opts=('-w:View in browser')
# _describe 'options' opts
_describe 'trace' traces ;;
esac
elif (( CURRENT > 4 )); then
case "${words[2]}::${words[3]}" in
@ -105,6 +121,12 @@ function _omz {
valid_plugins=(${valid_plugins:|args})
_describe 'plugin' valid_plugins ;;
trace::view)
local -a opts traces
# opts=('-w:View in browser')
traces=("${ZSH_CACHE_DIR}/.traces/"*.log(N:t))
# _describe 'options' opts
_describe 'trace' traces ;;
esac
fi
@ -746,7 +768,7 @@ function _omz::theme {
(( $# > 0 && $+functions[$0::$1] )) || {
cat >&2 <<EOF
Usage: ${(j: :)${(s.::.)0#_}} <command> [options]
$#traces
Available commands:
list List all available Oh My Zsh themes
@ -882,6 +904,150 @@ function _omz::theme::use {
[[ $1 = random ]] || unset RANDOM_THEME
}
function _omz::trace {
(( $# > 0 && $+functions[$0::$1] )) || {
cat >&2 <<EOF
Usage: ${(j: :)${(s.::.)0#_}} <command> [options]
Available commands:
clean [-a] Delete old or all traces
list List traces
off Turn debug tracing off
on Turn debug tracing on
toggle Toggle debug tracing
view View trace in browser
EOF
return 1
}
local command="$1"
shift
$0::$command "$@"
}
function _omz::trace::on {
'builtin' ':' > "${OMZ_TRACES}/.enabled"
print -ru2 '[oh-my-zsh] tracing enabled'
}
function _omz::trace::off {
'command' 'rm' "${OMZ_TRACES}/.enabled" 2>/dev/null
print -ru2 '[oh-my-zsh] tracing disabled'
}
function _omz::trace::toggle {
'builtin' 'test' -f "${OMZ_TRACES}/.enabled" \
&& _omz::trace::off || _omz::trace::on
}
function _omz::trace::clean {
if [[ -n "$1" && "$1" != "-a" ]]; then
echo >&2 "Usage: ${(j: :)${(s.::.)0#_}} [-a]"
return 1
fi
local -a traces
if [[ "$1" == "-a" ]]; then
traces=("${ZSH_CACHE_DIR}/.traces/"*.log(N))
if (( ! $#traces )); then
_omz::log info "there are no traces to remove"
return
fi
else
traces=("${ZSH_CACHE_DIR}/.traces/"*.log(m+7N))
if (( ! $#traces )); then
_omz::log info "there are no traces older than 7 days"
return
fi
fi
# Print found PR branches
print -l "Found these traces:" ${traces:t}
# Confirm before removing the branches
_omz::confirm "do you want remove them? [Y/n] "
# Only proceed if the answer is a valid yes option
[[ "$REPLY" != [yY$'\n'] ]] && return
_omz::log info "removing trace files..."
LANG= command rm -v "${traces[@]}"
}
function _omz::trace::list {
traces=("${ZSH_CACHE_DIR}/.traces/"*.log(N:t))
print -l -- ${(q-)traces}
}
function _omz::trace::view {
if [[ -z "$1" || "$1" = (-h|--help) ]] \
|| [[ "$1" == "--no-web" && -z "$2" ]]; then
echo >&2 "Usage: ${(j: :)${(s.::.)0#_}} [--no-web] <trace>"
return 1
fi
# If --no-web, open locally
local in_web=1
if [[ "$1" == "--no-web" ]]; then
in_web=0
shift
fi
# Get trace file
local trace="${OMZ_TRACES:-${ZSH_CACHE_DIR}/.traces}/$1"
if [[ ! -f "$trace" ]]; then
_omz::log error "Trace file not found: $trace"
return 1
fi
# If not in web mode, just display the trace file
if (( ! in_web )); then
# Enrich the file display depending on the tools we have
# - bat: https://github.com/sharkdp/bat
# - less: typical pager command
case 1 in
${+commands[bat]}) bat -l sh --style plain "$trace" ;;
${+commands[less]}) less "$trace" ;;
*) cat "$trace" ;;
esac
return $?
fi
# Load functions for web mode
autoload -Uz serve-file-and-quit omz_urlencode
local baseurl="https://trace.ohmyz.sh"
# Serve the trace file and get its URL
local profileURL
profileURL="$(serve-file-and-quit "$trace" 30 "$baseurl" 2>/dev/null)"
# If successful, open the trace viewer with the trace file URL
if [[ $? -eq 0 ]]; then
# Give the server some time to start
sleep 0.2
# -r and -P encode any special characters in the filename
local title="$(omz_urlencode -r -P "${trace:t}")"
_omz::log info "Opening ${baseurl} with trace file: '$trace' ..."
open_command "${baseurl}/#profileURL=${profileURL}&title=${title}"
return 0
fi
# Fallback: just open the trace viewer and let the user upload the file manually
_omz::log error "could not start HTTP server. Falling back to manual upload."
_omz::log info "Opening ${baseurl} ... Please manually upload the trace file from this path:"
_omz::log info "$trace"
open_command "$baseurl"
}
function _omz::update {
# Check if git command is available
(( $+commands[git] )) || {