335 lines
7.7 KiB
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)
|
|
}
|