shareserver
A generic Neovim plugin for keeping server processes alive across multiple Neovim instances using reference counting with automatic grace period support.
Features
- Multiple server management: Manage multiple servers simultaneously with named configurations
- Shared server management: One server process shared across multiple Neovim instances
- Reference counting: Servers stay alive as long as at least one Neovim instance is attached
- Grace period support: Servers can stay alive for a configurable period after all clients disconnect
- Lazy loading: Optionally only attach to servers if they're already running
- Automatic lifecycle: Servers start on VimEnter, stop on VimLeave with grace period
- Manual control: Start, stop, restart, and query status of named servers
- Flexible configuration: Configure command, args, working directory, and idle timeout per server
- Status monitoring: Check server status and get PID/refcount/grace period info
- Built-in commands: User commands are automatically created for easy server management
- Robust state management: Two-lockfile architecture with stale lock cleanup and atomic operations
Why Use SharedServer?
SharedServer solves a common problem: efficiently managing long-running service processes across multiple instances of your editor or between different tools.
Key Benefits
🔄 Reuse Servers Between Processes
- Start a service once (e.g., ChromaDB, Redis, development server)
- Share it across multiple Neovim instances
- Automatic reference counting ensures it stays alive as long as any instance needs it
⏱️ Smart Lifecycle Management
- Servers automatically shut down when no longer needed
- Configurable grace periods keep services warm for quick restarts
- Dead client detection prevents resource leaks from crashed processes
🎯 Built for Neovim
- Zero-configuration lifecycle hooks (
VimEnter/VimLeave) - Automatic attachment to existing servers
- Rich status monitoring with
:ServerStatus - Optional lazy loading for expensive services
🔧 Beyond Neovim
- Shell script integration via
sharedserverCLI - Use it as a service wrapper in any program
- Replace manual process management or shell scripts
- Works standalone or integrated with your editor
Example Use Cases
Replace manual server management:
# Instead of this fragile pattern:
||
&
# Do your work...
# Use sharedserver:
# Do your work...
Share expensive services:
-- ChromaDB takes 10s to start, costs 2GB RAM
-- Without sharedserver: Every Neovim instance starts its own (slow, wasteful)
-- With sharedserver: One instance shared by all, 30min grace period after last editor closes
require.
Lazy-load heavy services:
-- Don't start expensive ML inference server until explicitly needed
require.
-- Start manually when needed: :ServerStart llm_server
Why Not Just Use systemd/launchd?
System services (systemd, launchd, etc.) are great for always-on infrastructure, but SharedServer is designed for on-demand development services:
| System Service | SharedServer |
|---|---|
| Always running, even when unused | Starts when needed, stops when done |
| Requires root/system configuration | User-space, no sudo needed |
| Global configuration files | Per-project configuration in your editor config |
| Manual start/stop commands | Automatic lifecycle tied to your workflow |
| One instance system-wide | Multiple isolated instances per project possible |
When to use system services:
- Production servers
- Always-on infrastructure (databases, web servers)
- Services needed by multiple users
When to use SharedServer:
- Development databases (ChromaDB, Redis for testing)
- Project-specific dev servers
- Heavy services you only need occasionally
- Services tied to your editor workflow
- When you want automatic cleanup after work
Requirements
- Neovim 0.5+
- plenary.nvim
- Rust toolchain (for building sharedserver) - install from rustup.rs
- macOS: Built-in
flockvia Rust (no additional dependencies) - Linux: Built-in
flockvia Rust (no additional dependencies)
Building from Source
The plugin requires building the Rust-based sharedserver binary:
The binary will be available at rust/target/release/sharedserver. The plugin will automatically find it in this location.
Installation via Cargo (Recommended)
The simplest way to install sharedserver for use with the Neovim plugin:
Or install from the repository:
# From repository root
Both methods install the binary to ~/.cargo/bin/sharedserver, which should be in your PATH. This is sufficient for Neovim plugin usage.
System-wide Installation (Optional)
Install system-wide if you want to use sharedserver outside of Neovim (e.g., in shell scripts, cron jobs, or other programs):
# Linux
# macOS with Homebrew
# User-local installation (no sudo)
Why system-wide?
- Use
sharedserverCLI from any shell script - Integrate with systemd, cron, or other system services
- Share servers between different tools (not just Neovim)
The plugin searches for sharedserver in the following order:
<plugin-dir>/rust/target/release/sharedserver(default after build)~/.local/bin/sharedserver/usr/local/bin/sharedserver/opt/homebrew/bin/sharedserver
Shell Completions
Generate shell completion scripts for sharedserver:
# Bash
# Zsh
# Then add to ~/.zshrc: fpath=(~/.zsh/completions $fpath)
# Fish
Installation
Using lazy.nvim:
Or for local development:
See EXAMPLES.md for more configuration examples and usage patterns.
Configuration
Setup Options
The setup() function accepts two parameters:
require.
- servers: A table of server configurations (see below)
- opts: Optional configuration table with the following options:
- commands (default:
true): Whether to automatically create user commands - notify: Notification preferences (see below)
- commands (default:
Notification Configuration
Control when the plugin shows notifications:
require.
By default, the plugin is quiet during normal operations (attach/detach) and only notifies when:
- A new server is started for the first time
- An error occurs
- A server exits unexpectedly (non-zero exit code)
Multiple Servers (Recommended)
require.
Disable Commands
To disable automatic command creation:
require.
Single Server (Backward Compatible)
require.
Server Configuration Options
For each server:
- command (required): Command to execute (can be full path or command in PATH)
- args (optional, default:
{}): Arguments to pass to the command - env (optional, default:
{}): Environment variables to set for the server process- Table format:
{KEY = "value", KEY2 = "value2"} - Variables are added to (not replacing) the inherited environment
- Useful for: API keys, debug flags, custom paths, feature toggles
- Table format:
- log_file (optional, default:
nil): Path to log file for server stdout/stderr- When specified, server output is redirected to this file for debugging
- stdin is always redirected to
/dev/null(required for detached servers) - Useful for: debugging server startup issues, monitoring server output
- Example:
log_file = "/tmp/myserver.log"
- lazy (optional, default:
false): Iftrue, only attach to server if already running, don't start a new one - working_dir (optional, default:
nil): Working directory for the server - idle_timeout (optional, default:
nil): Grace period duration after last client disconnects (e.g.,"30m","1h","2h30m") - on_start (optional): Callback function called with
(pid)when server starts - on_exit (optional): Callback function called with
(exit_code)when server exits
Lazy Loading
The lazy option is useful for servers that:
- You want to share between projects but not start automatically
- Might be started by external tools
- Are expensive to start and should only run when needed
require.
-- Later, manually start when needed:
vim..
Environment Variables
Pass custom environment variables to server processes:
require.
Environment variables are added to the inherited environment (not replacing it). The server receives all variables from the parent process plus your custom ones.
CLI Usage:
API
All API functions are available through require("sharedserver"):
local sharedserver = require
-- Setup servers and optionally enable commands
sharedserver.
-- Register a server after initial setup
sharedserver.
-- Manually control servers
sharedserver.
sharedserver.
sharedserver.
sharedserver.
-- Query server status
local status = sharedserver.
local all_statuses = sharedserver.
local server_names = sharedserver.
Commands
When commands are enabled (default), the following user commands are automatically created:
:ServerStart <name>- Start a named server:ServerStop <name>- Stop a named server:ServerRestart <name>- Restart a named server:ServerStatus [name]- Show server status in a floating window (all servers if no name given):ServerList- List all registered servers (same as:ServerStatuswith no args):ServerStopAll- Stop all servers
The :ServerStatus command displays a rich floating window with:
- Server name and running status (●/○)
- PID, refcount, and uptime for running servers
- Attached/detached state
- Lazy mode indicator
- Full command details when viewing a specific server
Press q or <Esc> to close the status window.
Example output of :ServerStatus:
╭──────────────────────────── Shared Servers ─────────────────────────────╮
│ NAME STATUS PID REFS UPTIME │
│ ────────────────────────────────────────────────────────────────────────│
│ ● chroma running 1234 2 2h 15m │
│ ● redis running 5678 1 45m 32s │
│ ⏳ postgres GRACE 9012 0 3h 22m │
│ ○ myserver stopped - - - [lazy] │
│ │
│ Press q or <Esc> to close │
╰───────────────────────────────────────────────────────────────────────────╯
Status indicators:
●Running with active clients (refcount > 0)⏳Grace period (refcount = 0, waiting for timeout)○Stopped
To disable command creation, pass { commands = false } to setup().
Detailed API Reference
setup(servers, opts)
setup(servers, opts)
Initialize and register servers. Accepts:
- servers: A table of named server configurations, or a single server configuration (backward compatible)
- opts: Optional table with options:
commands(default:true): Whether to create user commands
Example:
require.
register(name, config)
Register a new server after initial setup.
require.
start(name)
Manually start a named server. Returns true on success, false on failure.
local success = require.
stop(name)
Manually stop a named server.
require.
stop_all()
Stop all registered servers.
require.
restart(name)
Restart a named server.
require.
status(name)
Get the status of a named server.
local status = require.
if status.
status_all()
Get status of all registered servers.
local statuses = require.
for name, status in pairs
list()
Get a list of all registered server names.
local servers = require.
for _, name in ipairs
How It Works
The plugin uses sharedserver (Rust-based, bundled in rust/target/release/) for robust server lifecycle management with a two-lockfile architecture and automatic dead client detection.
Architecture
Two lockfiles per server (default location: $XDG_RUNTIME_DIR/sharedserver/ or /tmp/sharedserver/):
<name>.server.json: Persistent server state (PID, command, start time, grace period settings)<name>.clients.json: Active client tracking (refcount, client PIDs with metadata)
Three states:
- ACTIVE:
clients.jsonexists (refcount > 0), server running normally - GRACE:
clients.jsondeleted (refcount = 0) but server still alive, waiting for timeout or new client - STOPPED: Both files deleted, server terminated
Lifecycle Flow
-
Neovim starts (
VimEnter):- Plugin checks if server exists using
sharedserver check <name> - For non-lazy servers:
- If exists: Increments refcount via
sharedserver incref(attaches to existing) - If not: Starts via
sharedserver start <name> -- <command> <args>
- If exists: Increments refcount via
- For lazy servers:
- If exists: Attaches (increments refcount)
- If not: Does nothing (waits for manual start)
- Plugin checks if server exists using
-
Multiple Neovim instances:
- Each instance calls
sharedserver incref <name>on startup - Refcount tracked in
clients.json(e.g., 3 instances = refcount 3) - Server process itself is registered as a client
- Each instance calls
-
Neovim exits (
VimLeave):- Plugin calls
sharedserver decref <name>automatically - Refcount decremented
- If refcount reaches 0:
clients.jsondeleted → Server enters GRACE period- Watcher starts countdown timer (e.g., 30 minutes)
- If new client attaches: Grace cancelled, back to ACTIVE
- If grace expires: Server receives TERM signal, both lockfiles deleted
- Plugin calls
-
Dead client detection (automatic):
- Watcher polls every 5 seconds
- Checks each client PID with health checks (Linux:
/proc, macOS:proc_pidinfo()) - Automatically removes dead clients and recalculates refcount
- Prevents refcount leaks from crashed Neovim instances
- If all clients die: Triggers grace period automatically
Grace Period Example
require.
Timeline:
- T+0: First nvim starts → Server launched, refcount=1
- T+5: Second nvim starts → refcount=2
- T+10: First nvim exits → refcount=1
- T+15: Second nvim exits → refcount=0, enter GRACE period (30min countdown)
- T+20: Third nvim starts → refcount=1, grace cancelled, back to ACTIVE
- T+25: Third nvim exits → refcount=0, enter GRACE again
- T+55: Grace expires (30min after T+25) → Server terminated
Use Cases
- Database servers: ChromaDB, Redis, PostgreSQL for development
- Language servers: Custom LSP servers, code analysis tools
- Development servers: HTTP servers, WebSocket servers
- Background processes: File watchers, sync daemons
- Expensive services: Large ML models, heavy databases (use
lazy = true)
For detailed configuration examples and usage patterns, see EXAMPLES.md.
Shell Integration
The rust/target/release/sharedserver binary allows shell scripts and other programs to participate in the shared server lifecycle.
sharedserver Commands
The sharedserver binary provides a clean command-line interface for managing shared servers:
Everyday Commands:
use <name> [-- <command> [args...]]- Attach to server (starts if needed)unuse <name>- Detach from serverlist- Show all managed serversinfo <name> [--json]- Get server details (formatted or JSON)check <name>- Test if server exists (exit codes: 0=active, 1=grace, 2=stopped)completion <shell>- Generate shell completions (bash/zsh/fish)
Admin Commands (for troubleshooting):
admin start <name> --pid <pid> -- <command> [args...]- Manually start serveradmin stop <name> [--force]- Emergency stop (sends SIGTERM)admin incref <name> --pid <pid>- Manually increment refcountadmin decref <name> --pid <pid>- Manually decrement refcountadmin debug <name>- Show invocation logs
PID Behavior:
- User commands (
use,unuse):--piddefaults to parent process (the caller) - Admin commands:
--piddefaults to current process (must be specified)
Examples
# Start or attach to a server
# Detach from server
# Check server status
# List all servers
# Emergency stop (admin)
Two-Lockfile Architecture
Serverctl uses a two-lockfile design for robust lifecycle management:
-
<name>.server.json: Persistent while server is running- Contains: PID, command, start time, grace period settings, watcher PID
- Created when server starts
- Deleted only when server truly dies (after grace period)
-
<name>.clients.json: Exists only while clients are connected (refcount > 0)- Contains: refcount, map of client PIDs to metadata (attached timestamp, custom metadata)
- Created when first client attaches (or when server starts with itself as first client)
- Deleted when last client dies or decref's (triggers grace period)
- Watcher automatically removes dead client PIDs every 5 seconds
Grace Period
When the last client decrements refcount to 0, the server enters a grace period instead of immediately shutting down:
# Start server with 30-minute grace period
# Or 1 hour grace period
Grace period flow:
- Last client decrefs or dies →
clients.jsondeleted - Watcher enters GRACE mode, starts countdown timer (e.g., 30 minutes)
- If new client increfs during grace:
clients.jsonrecreated, timer cancelled, back to ACTIVE - If grace expires: Watcher sends SIGTERM, waits 5s, sends SIGKILL if needed,
server.jsondeleted
Shell Script Usage
Requirements:
- Rust toolchain (see Building from Source section)
Basic usage:
# Check if server exists
if ! ; then
# Start server with 30-minute grace period
&
fi
# Register as client (optional - for tracking only)
# Use server...
# Unregister when done
Neovim Integration
The plugin automatically uses sharedserver - no manual configuration needed:
require.
What happens internally:
- Plugin starts server with:
sharedserver start --grace-period 30m opencode -- opencode serve --port 4097 - Server survives 30 minutes after all Neovim instances close
- Auto-decref when Neovim exits
- Grace period can be cancelled if a new client attaches
- Dead clients are automatically cleaned up by the watcher
Lockfile location:
- Default:
$XDG_RUNTIME_DIR/sharedserver/or/tmp/sharedserver/ <name>.server.json- Server state<name>.clients.json- Client refcount- Set
SHAREDSERVER_LOCKDIRenvironment variable to override
Advanced: Manual sharedserver usage from Lua
If you need to call sharedserver directly (e.g., for custom workflows):
local sharedserver = vim.. .. "/../path/to/rust/target/release/sharedserver"
-- Check if server exists
local result = vim..
if vim.. == 0
-- Get server info as JSON
local json = vim..
local info = vim..
print
Debugging
Health Check Command
Run :checkhealth sharedserver in Neovim to verify your setup:
:checkhealth sharedserver
This checks:
- ✓ sharedserver binary is installed and accessible
- ✓ Binary version information
- ✓ Lock directory is accessible and writable
- ✓ Plugin API is loaded correctly
- ✓ Current status of configured servers
Automatic Health Monitoring
The plugin automatically monitors servers for 3 seconds after startup. If a server exits immediately, you'll receive an error notification:
Error: sharedserver: 'myserver' died unexpectedly after start
This helps catch configuration issues that would otherwise fail silently.
Capturing Server Output
Important: Server stdout/stderr go directly to Neovim's terminal by design (transparency). To capture output for debugging, redirect it in your command:
require.
Then check /tmp/myserver.log for server output.
See DEBUGGING.md for comprehensive troubleshooting guide.
Common Issues
Server exits immediately:
- Redirect server output to a file to see error messages
- Compare with manually running the command in your shell
- Check environment variables (especially PATH)
- Verify file paths are absolute, not relative
- Check if server requires TTY or stdin
Command not found:
- Use absolute path:
command = "/usr/local/bin/myserver" - Or ensure command is in Neovim's PATH
Environment variables not working:
- Use
vim.fn.expand()for paths with~or$VAR - Example:
vim.fn.expand("$HOME") .. "/.config/app"
Port already in use:
- Check if another instance is running:
:ServerStatus - Stop it:
:ServerStop myserver - Or check system:
lsof -i :PORT
License
MIT