lifecycle

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 26, 2026 License: AGPL-3.0 Imports: 5 Imported by: 0

README

lifecycle

Go Report Card Go Doc License Release

lifecycle is a Go library for managing application shutdown signals and interactive terminal I/O robustly. It centralizes the "Dual Signal" logic and "Interruptible I/O" patterns originally extracted from Trellis and designed for any tool needing robust signal handling.

Vision

To provide a standard, leak-free way to handle CLI interruptions (Ctrl+C) and graceful shutdowns across Go CLI applications, handling OS idiosyncrasies (especially Windows CONIN$) transparently.

Installation

go get github.com/aretw0/lifecycle

Features

  • SignalContext: Differentiates between SIGINT (User Interrupt) and SIGTERM (System Shutdown).
    • SIGINT: Captured but doesn't cancel context immediately (allows "Wait, are you sure?" logic).
    • SIGTERM: Cancels context immediately (standard graceful shutdown).
  • TermIO:
    • InterruptibleReader: Wraps io.Reader to allow Read() calls to be abandoned when a context is cancelled (avoids goroutine leaks).
    • Platform Aware: Automatically uses CONIN$ on Windows to ensure signals are received during blocking reads.

Usage

Signal Context
package main

import (
    "context"
    "fmt"
    "github.com/aretw0/lifecycle"
)

func main() {
    // captures SIGINT/SIGTERM
    ctx := lifecycle.NewSignalContext(context.Background())
    defer ctx.Cancel() 

    <-ctx.Done()
    
    // Check why we stopped
    if sig := ctx.Signal(); sig != nil {
        fmt.Printf("Stopped by signal: %v\n", sig)
    }
}
Interruptible I/O
package main

import (
    "context"
    "fmt"
    "github.com/aretw0/lifecycle"
)

func main() {
    ctx := context.Background() // or SignalContext
    
    // Smart Open (handles Windows CONIN$)
    reader, _ := lifecycle.OpenTerminal()
    
    // Wrap to respect context cancellation
    r := lifecycle.NewInterruptibleReader(reader, ctx.Done())

    buf := make([]byte, 1024)
    n, err := r.Read(buf)
    if lifecycle.IsInterrupted(err) {
        fmt.Println("Read cancelled!")
        return
    }
    fmt.Printf("Read: %s\n", buf[:n])
}

Documentation

Documentation

Overview

Package lifecycle provides a centralized library for managing application lifecycles and interactive I/O.

Dual Signal Context

Standard Go `signal.NotifyContext` cancels on the first signal. `lifecycle` distinguishes between:

  • SIGINT (Ctrl+C): "Soft" interrupt. Captures the signal but keeps the Context active. Allows the application to decide whether to pause, confirm exit, or ignore.
  • SIGTERM: "Hard" stop. Cancels the Context immediately, triggering graceful shutdown.

Interruptible I/O

On many systems (especially Windows), reading from `os.Stdin` blocks the goroutine indefinitely, preventing clean cancellation. `lifecycle` provides `OpenTerminal` (using `CONIN$`) and `NewInterruptibleReader` to ensure I/O operations respect `context.Context` cancellation.

Usage

ctx := lifecycle.NewSignalContext(context.Background())
defer ctx.Cancel()

// Safe terminal reading
term, _ := lifecycle.OpenTerminal()
reader := lifecycle.NewInterruptibleReader(term, ctx.Done())

See examples/demo for a full interactive application.

Index

Examples

Constants

This section is empty.

Variables

View Source
var Version string

Functions

func IsInterrupted

func IsInterrupted(err error) bool

IsInterrupted checks if an error indicates an interruption (Context Canceled, EOF, etc.). Alias for pkg/termio.IsInterrupted.

func NewInterruptibleReader

func NewInterruptibleReader(base io.Reader, cancel <-chan struct{}) *termio.InterruptibleReader

NewInterruptibleReader returns a reader that checks the cancel channel before/after blocking reads. Alias for pkg/termio.NewInterruptibleReader.

func NewSignalContext

func NewSignalContext(parent context.Context) *signal.Context

NewSignalContext creates a context that cancels on SIGTERM but captures SIGINT. Alias for pkg/signal.NewContext.

Example

ExampleNewSignalContext demonstrates how to use the Dual Signal context. Note: This example is illustrative; in a real run, it waits for SIGINT/SIGTERM.

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/aretw0/lifecycle"
)

func main() {
	// Create a context that listens for signals.
	ctx := lifecycle.NewSignalContext(context.Background())

	// For checking output deterministically in this example, we cancel manually
	// after a short delay, allowing "work" to happen first.
	go func() {
		time.Sleep(50 * time.Millisecond)
		ctx.Cancel()
	}()

	// Simulate work
	select {
	case <-ctx.Done():
		fmt.Println("Context cancelled too early")
	case <-time.After(10 * time.Millisecond):
		fmt.Println("Doing work...")
	}

}
Output:

Doing work...

func OpenTerminal

func OpenTerminal() (io.ReadCloser, error)

OpenTerminal checks for text input capability and returns a Reader. On Windows, it tries to open CONIN$. Alias for pkg/termio.Open.

Example

ExampleOpenTerminal demonstrates how to open the terminal safely.

package main

import (
	"fmt"

	"github.com/aretw0/lifecycle"
)

func main() {
	// OpenTerminal handles OS-specific logic (like CONIN$ on Windows)
	reader, err := lifecycle.OpenTerminal()
	if err != nil {
		fmt.Printf("Error opening terminal: %v\n", err)
		return
	}
	defer reader.Close()

	fmt.Println("Terminal opened successfully")

	// Wrap with InterruptibleReader to respect context cancellation
	// r := lifecycle.NewInterruptibleReader(reader, ctx.Done())

}
Output:

Terminal opened successfully

func UpgradeTerminal added in v0.1.1

func UpgradeTerminal(r io.Reader) (io.Reader, error)

UpgradeTerminal checks if the provided reader is a terminal and returns a safe reader (e.g. CONIN$ on Windows). If not a terminal, returns the original reader.

Types

This section is empty.

Directories

Path Synopsis
examples
demo command
pkg

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL