whoseport: Find What's Listening on a Port (With Working Directory)

ss -tlnp and lsof -i :PORT tell you the PID and command name, but for Node.js or Python processes, "node" or "python" alone doesn't tell you which project is running. The working directory is what you actually need — and it's sitting right there in /proc/$pid/cwd.

Put this in ~/.bashrc:

whoseport() {
  if [ -z "$1" ]; then
    sudo ss -tlnp | tail -n +2 | while read -r line; do
      port=$(echo "$line" | grep -oP ':\K[0-9]+(?=\s)')
      pid=$(echo "$line" | grep -oP 'pid=\K[0-9]+' | head -1 | tr -dc '0-9')
      [ -n "$pid" ] && echo "PORT: $port | PID: $pid | CMD: $(ps -p $pid -o comm=) | CWD: $(readlink /proc/$pid/cwd)"
    done
  else
    whoseport | grep 'PORT: '$1
  fi
}

Usage:

$ whoseport 6173
PORT: 6173 | PID: 1326886 | CMD: node | CWD: /home/davidw/Projects/ccode_viewer/server

$ whoseport          # list all listening ports
PORT: 61217 | PID: 1356 | CMD: tailscaled | CWD: /
PORT: 22     | PID: 1385  | CMD: sshd      | CWD: /

A few things that went wrong before arriving at this version:

Don't use an alias for this — $1 in a single-quoted alias gets swallowed by inner sh -c calls, and local variables don't survive xargs boundaries. A function avoids both problems. The tr -dc '0-9' on the PID is not cosmetic — ss output can carry trailing whitespace or newlines that break ps -p with a "process ID list syntax error".

Git Worktree: Checkout an Existing Branch Into a Separate Directory

Need to work on two branches simultaneously without stashing or cloning? git worktree creates a separate working directory that shares the same .git — no duplicate objects, no wasted space.

git worktree add <path> <branch>

For example, to checkout feature/login into a sibling directory:

git worktree add ../my-feature feature/login

This creates ../my-feature with the branch checked out and ready to work. Commits, branches, and remotes are shared — it's one repository, multiple workspaces.

Day-to-day operations:

  • git worktree list — see all linked worktrees
  • git worktree remove <path> — clean up when done

One restriction: the same branch cannot be checked out in two worktrees at once. Git enforces this to prevent conflicting writes to the ref.

WSL2 Won't Run on EC2 Windows Server? Use WSL1

Most EC2 Windows Server instances don't expose nested virtualization to the guest OS. WSL2 requires Hyper-V extensions, so it simply won't start. WSL1 works fine — it's a translation layer, not a VM.

Enable the WSL feature and reboot in an Admin powershell window:

Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux
Restart-Computer

After reboot, pin the default version to 1:

wsl --set-default-version 1

On Windows Server, Add-AppxPackage is often broken or unavailable, so the standard wsl --install route fails. Use wsl --import with a rootfs tarball instead:

Now use a non-admin powershell:

mkdir $HOME\WSL
mkdir $HOME\WSL\Ubuntu

Then:

wsl --import Ubuntu $HOME\WSL\Ubuntu .\ubuntu.tar.gz --version 1

What you lose with WSL1: no Docker, no real Linux kernel, no systemd, weaker filesystem compatibility. Fine for CLI tooling, scripting, and build environments. If you need a real kernel, spin up a native Linux EC2 instance instead.

Auto-run startup scripts in Oracle 19c Docker to keep disk clean

Oracle's diagnostic files (trace, incident dumps, listener logs) grow without bound inside the container. If you're running doctorkirk/oracle-19c locally, they'll silently eat your disk.

The fix: mount a startup script into /opt/oracle/scripts/startup/ — this directory runs on every container start (unlike /docker-entrypoint-initdb.d/ which only runs on first database creation).

# docker-compose.yml
volumes:
  - ./startup-scripts:/opt/oracle/scripts/startup
# startup-scripts/init.sh (chmod +x)
#!/bin/bash
echo "=== Oracle startup config starting ==="

# Set ADR purge policies — auto-delete old diagnostic files
adrci <<EOF
set home diag/rdbms/orcl/ORCL
set control (SHORTP_POLICY = 24)
set control (LONGP_POLICY = 48)
set home diag/tnslsnr/$(hostname)/listener
set control (SHORTP_POLICY = 24)
set control (LONGP_POLICY = 48)
EOF
echo "ADR purge policy set: SHORT=24h, LONG=48h"

# Disable listener log (it's huge and rarely needed locally)
lsnrctl set log_status off
echo "Listener logging disabled"

echo "=== Oracle startup config done ==="

SHORTP_POLICY controls how long short-lived files (trace, cdump) are kept. LONGP_POLICY controls incident dumps and core files. 24h/48h is aggressive but fine for a dev environment.

Add echo markers so you can verify it ran: docker compose logs oracle19c | grep "startup config".

One more thing — the diag directory lives in the container's writable layer by default. If Oracle goes haywire (e.g. ORA-600 loop), it can still fill up overlay2 and tank Docker entirely. Mount it out as a safety valve:

- ./oracle-19c/diag/:/opt/oracle/diag

tmux mouse selection not copying to clipboard? Bind MouseDragEnd + MouseUp

With set -g mouse on in tmux, dragging to select text enters copy mode but the selection stays trapped inside tmux — it never reaches the system clipboard. The fix is a two-line binding that pipes the selection through xclip:

# ~/.tmux.conf
set -g mouse on

# Auto-copy on mouse drag end
bind -T copy-mode MouseDragEnd1Pane send -X copy-pipe-and-cancel "xclip -selection clipboard"
# Fallback: also copy on simple mouse-up (covers cases where drag is too short)
bind -T copy-mode MouseUp1Pane send -X copy-pipe-and-cancel "xclip -selection clipboard"

You need xclip installed (sudo apt install xclip). Reload with tmux source-file ~/.tmux.conf.

The copy-pipe-and-cancel does three things at once: grabs the selection, pipes it to xclip -selection clipboard, then exits copy mode (which clears the highlight). That's how you know it worked — the highlight disappearing means the text is in your clipboard.

Why two bindings

MouseDragEnd1Pane only fires on a proper drag. If you click-and-release too quickly, tmux enters copy mode but the drag-end event never fires, leaving the highlight stuck. MouseUp1Pane catches those cases.

When it won't work

If a pane is running an application that captures mouse input (vim, htop, some CLI tools like GitHub Copilot), tmux forwards mouse events to that program and the bindings don't fire. Your fallback in those panes is holding Shift while selecting — that bypasses tmux entirely and uses the terminal emulator's native selection, but it crosses pane boundaries so the text may include content from adjacent panes.