Files
techircd/main.go

335 lines
7.7 KiB
Go

package main
import (
"flag"
"fmt"
"log"
"os"
"os/exec"
"os/signal"
"strconv"
"strings"
"syscall"
"time"
)
var DebugMode bool
const (
VERSION = "1.0.0"
PIDFILE = "techircd.pid"
CONFIGFILE = "config.json"
)
func main() {
// Parse command line arguments
if len(os.Args) < 2 {
showUsage()
os.Exit(1)
}
command := strings.ToLower(os.Args[1])
// Parse flags for the remaining arguments
flagSet := flag.NewFlagSet("techircd", flag.ExitOnError)
configFile := flagSet.String("config", CONFIGFILE, "Path to configuration file")
daemon := flagSet.Bool("daemon", false, "Run as daemon (background)")
verbose := flagSet.Bool("verbose", false, "Enable verbose logging")
debug := flagSet.Bool("debug", false, "Enable extremely detailed debug logging (shows all IRC messages)")
port := flagSet.Int("port", 0, "Override port from config")
// Parse remaining arguments
flagSet.Parse(os.Args[2:])
switch command {
case "start":
startServer(*configFile, *daemon, *verbose, *debug, *port)
case "stop":
stopServer()
case "restart":
stopServer()
time.Sleep(2 * time.Second)
startServer(*configFile, *daemon, *verbose, *debug, *port)
case "status":
showStatus()
case "reload":
reloadConfig()
case "version", "-v", "--version":
fmt.Printf("TechIRCd version %s\n", VERSION)
case "help", "-h", "--help":
showUsage()
default:
fmt.Printf("Unknown command: %s\n", command)
showUsage()
os.Exit(1)
}
}
func showUsage() {
fmt.Printf(`TechIRCd %s - Modern IRC Server
Usage: %s <command> [options]
Commands:
start Start the IRC server
stop Stop the IRC server
restart Restart the IRC server
status Show server status
reload Reload configuration
version Show version information
help Show this help message
Options:
-config <file> Path to configuration file (default: config.json)
-daemon Run as daemon (background process)
-verbose Enable verbose logging
-debug Enable extremely detailed debug logging (shows all IRC messages)
-port <port> Override port from configuration
Examples:
%s start # Start with default config
%s start -config custom.json # Start with custom config
%s start -daemon # Start as background daemon
%s stop # Stop the server
%s status # Check if server is running
`, VERSION, os.Args[0], os.Args[0], os.Args[0], os.Args[0], os.Args[0], os.Args[0])
}
func startServer(configFile string, daemon, verbose, debug bool, port int) {
// Check if server is already running
if isRunning() {
fmt.Println("TechIRCd is already running")
os.Exit(1)
}
// Load configuration
config, err := LoadConfig(configFile)
if err != nil {
log.Printf("Failed to load config from %s, using defaults: %v", configFile, err)
config = DefaultConfig()
if err := SaveConfig(config, configFile); err != nil {
log.Printf("Failed to save default config: %v", err)
}
}
// Override port if specified
if port > 0 {
config.Server.Listen.Port = port
fmt.Printf("Port overridden to %d\n", port)
}
// Validate configuration
if err := config.Validate(); err != nil {
log.Fatalf("Configuration validation failed: %v", err)
}
config.SanitizeConfig()
if verbose {
log.Println("Configuration validated successfully")
}
// Set global debug mode
DebugMode = debug
if debug {
log.Println("Debug mode enabled - will log all IRC messages")
}
// Start as daemon if requested
if daemon {
startDaemon(configFile, verbose, debug, port)
return
}
// Write PID file
if err := writePidFile(); err != nil {
log.Fatalf("Failed to write PID file: %v", err)
}
defer removePidFile()
// Create server
server := NewServer(config)
// Setup signal handling for graceful shutdown with forced shutdown capability
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
shutdownInProgress := false
go func() {
for sig := range c {
switch sig {
case syscall.SIGHUP:
log.Println("Received SIGHUP, reloading configuration...")
// Reload config here if needed
case os.Interrupt, syscall.SIGTERM:
if shutdownInProgress {
log.Println("Received second interrupt signal, forcing immediate shutdown...")
removePidFile()
os.Exit(1) // Force exit
}
shutdownInProgress = true
log.Println("Shutting down server...")
// Start shutdown with timeout
shutdownComplete := make(chan bool, 1)
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("Panic during shutdown: %v", r)
}
shutdownComplete <- true
}()
server.Shutdown()
}()
// Wait for graceful shutdown or force after timeout
select {
case <-shutdownComplete:
log.Println("Graceful shutdown completed")
case <-time.After(10 * time.Second):
log.Println("Shutdown timeout reached, forcing exit...")
}
removePidFile()
os.Exit(0)
}
}
}()
// Start the server
fmt.Printf("Starting TechIRCd %s on %s:%d\n", VERSION, config.Server.Listen.Host, config.Server.Listen.Port)
if config.Server.Listen.EnableSSL {
fmt.Printf("SSL enabled on port %d\n", config.Server.Listen.SSLPort)
}
if err := server.Start(); err != nil {
log.Fatalf("Failed to start server: %v", err)
}
}
func startDaemon(configFile string, verbose, debug bool, port int) {
// Build command arguments
args := []string{os.Args[0], "start", "-config", configFile}
if verbose {
args = append(args, "-verbose")
}
if debug {
args = append(args, "-debug")
}
if port > 0 {
args = append(args, "-port", strconv.Itoa(port))
}
// Start as background process
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdout = nil
cmd.Stderr = nil
cmd.Stdin = nil
if err := cmd.Start(); err != nil {
log.Fatalf("Failed to start daemon: %v", err)
}
fmt.Printf("TechIRCd started as daemon (PID: %d)\n", cmd.Process.Pid)
}
func stopServer() {
pid, err := readPidFile()
if err != nil {
fmt.Println("TechIRCd is not running")
return
}
process, err := os.FindProcess(pid)
if err != nil {
fmt.Println("TechIRCd is not running")
removePidFile()
return
}
// Send SIGTERM for graceful shutdown
if err := process.Signal(syscall.SIGTERM); err != nil {
fmt.Println("TechIRCd is not running")
removePidFile()
return
}
// Wait for process to stop
for i := 0; i < 10; i++ {
if !isRunning() {
fmt.Println("TechIRCd stopped")
return
}
time.Sleep(500 * time.Millisecond)
}
// Force kill if still running
process.Signal(syscall.SIGKILL)
fmt.Println("TechIRCd force stopped")
removePidFile()
}
func showStatus() {
if isRunning() {
pid, _ := readPidFile()
fmt.Printf("TechIRCd is running (PID: %d)\n", pid)
} else {
fmt.Println("TechIRCd is not running")
}
}
func reloadConfig() {
pid, err := readPidFile()
if err != nil {
fmt.Println("TechIRCd is not running")
return
}
process, err := os.FindProcess(pid)
if err != nil {
fmt.Println("TechIRCd is not running")
return
}
if err := process.Signal(syscall.SIGHUP); err != nil {
fmt.Println("Failed to reload configuration")
return
}
fmt.Println("Configuration reload signal sent")
}
func isRunning() bool {
pid, err := readPidFile()
if err != nil {
return false
}
process, err := os.FindProcess(pid)
if err != nil {
return false
}
// Send signal 0 to check if process exists
err = process.Signal(syscall.Signal(0))
return err == nil
}
func writePidFile() error {
pid := os.Getpid()
return os.WriteFile(PIDFILE, []byte(strconv.Itoa(pid)), 0644)
}
func readPidFile() (int, error) {
data, err := os.ReadFile(PIDFILE)
if err != nil {
return 0, err
}
return strconv.Atoi(strings.TrimSpace(string(data)))
}
func removePidFile() {
os.Remove(PIDFILE)
}