Add abort signals; rename guest to toes-cli

This commit is contained in:
Chris Wanstrath 2026-02-28 13:34:14 -08:00
parent dc570cc6e9
commit a87f0a9651
7 changed files with 44 additions and 28 deletions

View File

@ -1,8 +1,8 @@
/*
* libnss_toes - NSS module that resolves unknown usernames to a shared account.
*
* Any username not found in /etc/passwd gets mapped to a toes-guest entry
* with home=/home/toes and shell=/usr/local/bin/toes.
* Any username not found in /etc/passwd gets mapped to a toes-cli entry
* with home=/home/toes-cli and shell=/usr/local/bin/toes.
*
* Build:
* gcc -shared -o libnss_toes.so.2 libnss_toes.c -fPIC
@ -17,14 +17,14 @@
#include <string.h>
#include <errno.h>
#define GUEST_UID 65534
#define GUEST_GID 65534
#define GUEST_HOME "/home/toes"
#define GUEST_UID 3001
#define GUEST_GID 3001
#define GUEST_HOME "/home/toes-cli"
#define GUEST_SHELL "/usr/local/bin/toes"
#define GUEST_GECOS "Toes Guest"
#define GUEST_GECOS "Toes CLI"
static const char *skip_users[] = {
"root", "toes", "nobody", "daemon", "bin", "sys", "sync",
"root", "toes", "toes-cli", "nobody", "daemon", "bin", "sys", "sync",
"games", "man", "lp", "mail", "news", "uucp", "proxy",
"www-data", "backup", "list", "irc", "gnats", "systemd-network",
"systemd-resolve", "messagebus", "sshd", "_apt",

View File

@ -75,11 +75,19 @@ if [ ! -f "$TOES_SHELL" ]; then
echo " Create it with: ln -sf /path/to/toes/cli $TOES_SHELL"
fi
# Ensure /home/toes exists for guest sessions
if [ ! -d /home/toes ]; then
mkdir -p /home/toes
chmod 755 /home/toes
echo " Created /home/toes"
# Create toes-cli system user for guest SSH sessions
if ! id toes-cli &>/dev/null; then
useradd --system --uid 3001 --home-dir /home/toes-cli --shell /usr/local/bin/toes --create-home toes-cli
echo " Created toes-cli user"
else
echo " toes-cli user already exists"
fi
# Ensure /home/toes-cli exists for guest sessions
if [ ! -d /home/toes-cli ]; then
mkdir -p /home/toes-cli
chmod 755 /home/toes-cli
echo " Created /home/toes-cli"
fi
# 6. Restart sshd
@ -87,4 +95,5 @@ echo " Restarting sshd..."
systemctl restart sshd || service ssh restart || true
echo "==> Done. Any SSH user will now get the toes CLI."
echo " SSH users are mapped to the toes-cli account (UID 3001)."
echo " toes@toes.local still gets a regular shell."

View File

@ -1,6 +1,6 @@
import type { LogLine } from '@types'
import color from 'kleur'
import { get, handleError, makeUrl, post } from '../http'
import { activeSignal, get, handleError, makeUrl, post } from '../http'
import { resolveAppName } from '../name'
interface CronJobSummary {
@ -195,7 +195,7 @@ const printCronLog = (line: LogLine) =>
async function tailCronLogs(app: string, grep?: string) {
try {
const url = makeUrl(`/api/apps/${app}/logs/stream`)
const res = await fetch(url)
const res = await fetch(url, { signal: activeSignal })
if (!res.ok) {
console.error(`App not found: ${app}`)
return

View File

@ -1,5 +1,5 @@
import type { LogLine } from '@types'
import { get, handleError, makeUrl } from '../http'
import { activeSignal, get, handleError, makeUrl } from '../http'
import { resolveAppName } from '../name'
interface LogOptions {
@ -120,7 +120,7 @@ export async function logApp(arg: string | undefined, options: LogOptions) {
export async function tailLogs(name: string, grep?: string) {
try {
const url = makeUrl(`/api/apps/${name}/logs/stream`)
const res = await fetch(url)
const res = await fetch(url, { signal: activeSignal })
if (!res.ok) {
console.error(`App not found: ${name}`)
return

View File

@ -5,7 +5,7 @@ import color from 'kleur'
import { diffLines } from 'diff'
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, unlinkSync, watch, writeFileSync } from 'fs'
import { dirname, join } from 'path'
import { del, download, get, getManifest, handleError, makeUrl, post, put } from '../http'
import { activeSignal, del, download, get, getManifest, handleError, makeUrl, post, put } from '../http'
import { confirm, prompt } from '../prompts'
import { getAppName, getAppPackage, isApp, resolveAppName } from '../name'
@ -592,7 +592,7 @@ export async function syncApp() {
const url = makeUrl(`/api/sync/apps/${appName}/watch`)
let res: Response
try {
res = await fetch(url)
res = await fetch(url, { signal: activeSignal })
if (!res.ok) {
console.error(`Failed to connect to server: ${res.status} ${res.statusText}`)
watcher.close()
@ -967,7 +967,7 @@ interface VersionsResponse {
async function getVersions(appName: string): Promise<VersionsResponse | null> {
try {
const res = await fetch(makeUrl(`/api/sync/apps/${appName}/versions`))
const res = await fetch(makeUrl(`/api/sync/apps/${appName}/versions`), { signal: activeSignal })
if (res.status === 404) {
console.error(`App not found: ${appName}`)
return null

View File

@ -1,6 +1,8 @@
import type { Manifest } from '@types'
const DEFAULT_HOST = process.env.DEV ? 'http://localhost:3000' : 'http://toes.local'
export let activeSignal: AbortSignal | undefined
const normalizeUrl = (url: string) =>
url.startsWith('http://') || url.startsWith('https://') ? url : `http://${url}`
@ -17,6 +19,10 @@ export const HOST = process.env.TOES_URL
? normalizeUrl(process.env.TOES_URL)
: DEFAULT_HOST
export const clearSignal = () => { activeSignal = undefined }
export const setSignal = (signal: AbortSignal) => { activeSignal = signal }
export function makeUrl(path: string): string {
return `${HOST}${path}`
}
@ -36,7 +42,7 @@ export function handleError(error: unknown): void {
export async function get<T>(url: string): Promise<T | undefined> {
try {
const res = await fetch(makeUrl(url))
const res = await fetch(makeUrl(url), { signal: activeSignal })
if (!res.ok) {
const text = await res.text()
const msg = tryParseError(text) ?? `${res.status} ${res.statusText}`
@ -50,7 +56,7 @@ export async function get<T>(url: string): Promise<T | undefined> {
export async function getManifest(appName: string): Promise<{ exists: boolean, manifest?: Manifest, version?: string } | null> {
try {
const res = await fetch(makeUrl(`/api/sync/apps/${appName}/manifest`))
const res = await fetch(makeUrl(`/api/sync/apps/${appName}/manifest`), { signal: activeSignal })
if (res.status === 404) return { exists: false }
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
const data = await res.json()
@ -68,6 +74,7 @@ export async function post<T, B = unknown>(url: string, body?: B): Promise<T | u
method: 'POST',
headers: body !== undefined ? { 'Content-Type': 'application/json' } : undefined,
body: body !== undefined ? JSON.stringify(body) : undefined,
signal: activeSignal,
})
if (!res.ok) {
const text = await res.text()
@ -85,6 +92,7 @@ export async function put(url: string, body: Buffer | Uint8Array): Promise<boole
const res = await fetch(makeUrl(url), {
method: 'PUT',
body: body as BodyInit,
signal: activeSignal,
})
if (!res.ok) {
const text = await res.text()
@ -101,7 +109,7 @@ export async function put(url: string, body: Buffer | Uint8Array): Promise<boole
export async function download(url: string): Promise<Buffer | undefined> {
try {
const fullUrl = makeUrl(url)
const res = await fetch(fullUrl)
const res = await fetch(fullUrl, { signal: activeSignal })
if (!res.ok) {
const text = await res.text()
const msg = tryParseError(text) ?? `${res.status} ${res.statusText}`
@ -117,6 +125,7 @@ export async function del(url: string): Promise<boolean> {
try {
const res = await fetch(makeUrl(url), {
method: 'DELETE',
signal: activeSignal,
})
if (!res.ok) {
const text = await res.text()

View File

@ -4,7 +4,7 @@ import * as readline from 'readline'
import color from 'kleur'
import { get, handleError, HOST } from './http'
import { clearSignal, get, handleError, HOST, setSignal } from './http'
import { program } from './setup'
import { STATE_ICONS } from './commands/manage'
@ -185,12 +185,10 @@ export async function shell(): Promise<void> {
const tokens = tokenize(input)
// Wrap fetch with AbortController for this command
// Set up AbortController for this command
activeAbort = new AbortController()
const originalFetch = globalThis.fetch
const signal = activeAbort.signal
globalThis.fetch = (url, init) =>
originalFetch(url, { ...init, signal })
setSignal(signal)
// Pause readline so commands can use their own prompts
rl.pause()
@ -215,7 +213,7 @@ export async function shell(): Promise<void> {
handleError(err)
}
} finally {
globalThis.fetch = originalFetch
clearSignal()
activeAbort = null
}