mirror of
https://github.com/ohmyzsh/ohmyzsh.git
synced 2026-02-11 05:39:45 +08:00
Merge 471958dfb3 into 41c5b9677a
This commit is contained in:
commit
bfbc88b86a
155
functions/serve-file-and-quit
Normal file
155
functions/serve-file-and-quit
Normal 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
58
hooks/zshenv
Normal 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"
|
||||
168
lib/cli.zsh
168
lib/cli.zsh
@ -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] )) || {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user