package papod import ( "bufio" "bytes" "crypto/hmac" "crypto/rand" "crypto/sha256" "database/sql" "encoding/binary" "encoding/hex" "errors" "flag" "fmt" "hash" "io" "io/ioutil" "log/slog" "math/big" "math/bits" "net" "os" "regexp" "runtime/debug" "sort" "strings" "sync" "syscall" "time" _ "github.com/mattn/go-sqlite3" ) /* Global variables */ var ( Hostname string Version string Colour string ) // FIXME: reorder var EmitActiveConnection = MakeGauge("active-connections") var EmitNicksInChannel = MakeGauge("nicks-in-channel") var EmitReceivedMessage = MakeCounter("received-message") var EmitWriteToClientError = MakeCounter("write-to-client") const pingFrequency = time.Duration(30) * time.Second const pongMaxLatency = time.Duration(5) * time.Second // FIXME: finish rewriting // // lastV7time is the last time we returned stored as: // // 52 bits of time in milliseconds since epoch // 12 bits of (fractional nanoseconds) >> 8 var lastV7Time int64 var timeMu sync.Mutex // getV7Time returns the time in milliseconds and nanoseconds / 256. // The returned (milli << (12 + seq)) is guaranteed to be greater than // (milli << (12 + seq)) returned by any previous call to getV7Time. // `seq` Sequence number is between 0 and 3906 (nanoPerMilli >> 8) func getV7Time(nano int64) (int64, int64) { const nanoPerMilli = 1000 * 1000 milli := nano / nanoPerMilli seq := (nano - (milli * nanoPerMilli)) >> 8 now := milli << (12 + seq) timeMu.Lock() defer timeMu.Unlock() if now <= lastV7Time { now = lastV7Time + 1 milli = now >> 12 seq = now & 0xfff } lastV7Time = now return milli, seq } const lengthUUID = 16 type UUID struct { bytes [lengthUUID]byte } func NewUUID() UUID { var buf [lengthUUID]byte _, err := io.ReadFull(rand.Reader, buf[7:]) if err != nil { panic(err) } buf[6] = (buf[6] & 0x0f) | 0x40 // Version 4 buf[8] = (buf[8] & 0x3f) | 0x80 // Variant is 10 t, s := getV7Time(time.Now().UnixNano()) buf[0] = byte(t >> 40) buf[1] = byte(t >> 32) buf[2] = byte(t >> 24) buf[3] = byte(t >> 16) buf[4] = byte(t >> 8) buf[5] = byte(t >> 0) buf[6] = 0x70 | (0x0f & byte(s >> 8)) buf[7] = byte(s) return UUID { bytes: buf } } func (uuid UUID) ToString() string { const dashCount = 4 const encodedLength = (lengthUUID * 2) + dashCount dst := [encodedLength]byte { 0, 0, 0, 0, 0, 0, 0, 0, '-', 0, 0, 0, 0, '-', 0, 0, 0, 0, '-', 0, 0, 0, 0, '-', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, } hex.Encode(dst[ 0:8], uuid.bytes[0:4]) hex.Encode(dst[ 9:13], uuid.bytes[4:6]) hex.Encode(dst[14:18], uuid.bytes[6:8]) hex.Encode(dst[19:23], uuid.bytes[8:10]) hex.Encode(dst[24:36], uuid.bytes[10:]) return string(dst[:]) } type LogLevel int8 const ( LevelNone LogLevel = 0 LevelError LogLevel = 1 LevelWarning LogLevel = 2 LevelInfo LogLevel = 3 LevelDebug LogLevel = 4 ) var Level LogLevel = LevelInfo var EmitMetric bool = true func Debug(message string, type_ string, args ...any) { if (Level < LevelDebug) { return } slog.Debug( message, append( []any { "id", NewUUID().ToString(), "kind", "log", "type", type_, }, args..., )..., ) } func Info(message string, type_ string, args ...any) { if (Level < LevelInfo) { return } slog.Info( message, append( []any { "id", NewUUID().ToString(), "kind", "log", "type", type_, }, args..., )..., ) } func Warning(message string, type_ string, args ...any) { if (Level < LevelWarning) { return } slog.Warn( message, append( []any { "id", NewUUID().ToString(), "kind", "log", "type", type_, }, args..., )..., ) } func Error(message string, type_ string, args ...any) { if (Level < LevelError) { return } slog.Error( message, append( []any { "id", NewUUID().ToString(), "kind", "log", "type", type_, }, args..., )..., ) } func Metric(type_ string, label string, args ...any) { if (!EmitMetric) { return } slog.Info( "_", append( []any { "id", NewUUID().ToString(), "kind", "metric", "type", type_, "label", label, }, args..., )..., ) } type Gauge struct { Inc func(...any) Dec func(...any) } var zero = big.NewInt(0) var one = big.NewInt(1) func MakeGauge(label string, staticArgs ...any) Gauge { count := big.NewInt(0) emitGauge := func(dynamicArgs ...any) { if count.Cmp(zero) == -1 { Error( "Gauge went negative", "process-metric", append( []any { "value", count }, append( staticArgs, dynamicArgs..., )..., )..., ) return // avoid wrong metrics being emitted } Metric( "gauge", label, // TODO: we'll have slices.Concat on Go 1.22 append( []any { "value", count }, append( staticArgs, dynamicArgs..., )..., )..., ) } return Gauge { Inc: func(dynamicArgs ...any) { count.Add(count, one) emitGauge(dynamicArgs...) }, Dec: func(dynamicArgs ...any) { count.Sub(count, one) emitGauge(dynamicArgs...) }, } } func MakeCounter(label string) func(...any) { return func(args ...any) { Metric( "counter", label, append([]any { "value", 1 }, args...)..., ) } } func SetLoggerOutput(w io.Writer) { slog.SetDefault(slog.New(slog.NewJSONHandler(w, &slog.HandlerOptions { AddSource: true, })).With( slog.Group( "info", "pid", os.Getpid(), "ppid", os.Getppid(), "puuid", NewUUID().ToString(), ), )) } func SetTraceback() { if os.Getenv("GOTRACEBACK") == "" { debug.SetTraceback("crash") } } func Fatal(err error) { Error( "Fatal error", "fatal-error", "error", err, "stack", string(debug.Stack()), ) syscall.Kill(os.Getpid(), syscall.SIGABRT) os.Exit(3) } func FatalIf(err error) { if err != nil { Fatal(err) } } /* Package pbkdf2 implements the key derivation function PBKDF2 as defined in RFC 2898 / PKCS #5 v2.0. A key derivation function is useful when encrypting data based on a password or any other not-fully-random data. It uses a pseudorandom function to derive a secure encryption key based on the password. While v2.0 of the standard defines only one pseudorandom function to use, HMAC-SHA1, the drafted v2.1 specification allows use of all five FIPS Approved Hash Functions SHA-1, SHA-224, SHA-256, SHA-384 and SHA-512 for HMAC. To choose, you can pass the `New` functions from the different SHA packages to pbkdf2.Key. */ // Key derives a key from the password, salt and iteration count, returning a // []byte of length keylen that can be used as cryptographic key. The key is // derived based on the method described as PBKDF2 with the HMAC variant using // the supplied hash function. // // For example, to use a HMAC-SHA-1 based PBKDF2 key derivation function, you // can get a derived key for e.g. AES-256 (which needs a 32-byte key) by // doing: // // dk := pbkdf2.Key([]byte("some password"), salt, 4096, 32, sha1.New) // // Remember to get a good random salt. At least 8 bytes is recommended by the // RFC. // // Using a higher iteration count will increase the cost of an exhaustive // search but will also make derivation proportionally slower. func PBKDF2Key( password []byte, salt []byte, iter int, keyLen int, h func() hash.Hash, ) []byte { prf := hmac.New(h, password) hashLen := prf.Size() numBlocks := (keyLen + hashLen - 1) / hashLen var buf [4]byte dk := make([]byte, 0, numBlocks*hashLen) U := make([]byte, hashLen) for block := 1; block <= numBlocks; block++ { // N.B.: || means concatenation, ^ means XOR // for each block T_i = U_1 ^ U_2 ^ ... ^ U_iter // U_1 = PRF(password, salt || uint(i)) prf.Reset() prf.Write(salt) buf[0] = byte(block >> 24) buf[1] = byte(block >> 16) buf[2] = byte(block >> 8) buf[3] = byte(block) prf.Write(buf[:4]) dk = prf.Sum(dk) T := dk[len(dk)-hashLen:] copy(U, T) // U_n = PRF(password, U_(n-1)) for n := 2; n <= iter; n++ { prf.Reset() prf.Write(U) U = U[:0] U = prf.Sum(U) for x := range U { T[x] ^= U[x] } } } return dk[:keyLen] } // Package scrypt implements the scrypt key derivation function as defined in // Colin Percival's paper "Stronger Key Derivation via Sequential Memory-Hard // Functions" (https://www.tarsnap.com/scrypt/scrypt.pdf). const maxInt = int(^uint(0) >> 1) // blockCopy copies n numbers from src into dst. func blockCopy(dst []uint32, src []uint32, n int) { copy(dst, src[:n]) } // blockXOR XORs numbers from dst with n numbers from src. func blockXOR(dst []uint32, src []uint32, n int) { for i, v := range src[:n] { dst[i] ^= v } } // salsaXOR applies Salsa20/8 to the XOR of 16 numbers from tmp and in, // and puts the result into both tmp and out. func salsaXOR(tmp *[16]uint32, in []uint32, out []uint32) { w0 := tmp[0] ^ in[0] w1 := tmp[1] ^ in[1] w2 := tmp[2] ^ in[2] w3 := tmp[3] ^ in[3] w4 := tmp[4] ^ in[4] w5 := tmp[5] ^ in[5] w6 := tmp[6] ^ in[6] w7 := tmp[7] ^ in[7] w8 := tmp[8] ^ in[8] w9 := tmp[9] ^ in[9] w10 := tmp[10] ^ in[10] w11 := tmp[11] ^ in[11] w12 := tmp[12] ^ in[12] w13 := tmp[13] ^ in[13] w14 := tmp[14] ^ in[14] w15 := tmp[15] ^ in[15] x0 := w0 x1 := w1 x2 := w2 x3 := w3 x4 := w4 x5 := w5 x6 := w6 x7 := w7 x8 := w8 x9 := w9 x10 := w10 x11 := w11 x12 := w12 x13 := w13 x14 := w14 x15 := w15 for i := 0; i < 8; i += 2 { x4 ^= bits.RotateLeft32(x0 + x12, 7) x8 ^= bits.RotateLeft32(x4 + x0, 9) x12 ^= bits.RotateLeft32(x8 + x4, 13) x0 ^= bits.RotateLeft32(x12 + x8, 18) x9 ^= bits.RotateLeft32(x5 + x1, 7) x13 ^= bits.RotateLeft32(x9 + x5, 9) x1 ^= bits.RotateLeft32(x13 + x9, 13) x5 ^= bits.RotateLeft32(x1 + x13, 18) x14 ^= bits.RotateLeft32(x10 + x6, 7) x2 ^= bits.RotateLeft32(x14 + x10, 9) x6 ^= bits.RotateLeft32(x2 + x14, 13) x10 ^= bits.RotateLeft32(x6 + x2, 18) x3 ^= bits.RotateLeft32(x15 + x11, 7) x7 ^= bits.RotateLeft32(x3 + x15, 9) x11 ^= bits.RotateLeft32(x7 + x3, 13) x15 ^= bits.RotateLeft32(x11 + x7, 18) x1 ^= bits.RotateLeft32(x0 + x3, 7) x2 ^= bits.RotateLeft32(x1 + x0, 9) x3 ^= bits.RotateLeft32(x2 + x1, 13) x0 ^= bits.RotateLeft32(x3 + x2, 18) x6 ^= bits.RotateLeft32(x5 + x4, 7) x7 ^= bits.RotateLeft32(x6 + x5, 9) x4 ^= bits.RotateLeft32(x7 + x6, 13) x5 ^= bits.RotateLeft32(x4 + x7, 18) x11 ^= bits.RotateLeft32(x10 + x9, 7) x8 ^= bits.RotateLeft32(x11 + x10, 9) x9 ^= bits.RotateLeft32(x8 + x11, 13) x10 ^= bits.RotateLeft32(x9 + x8, 18) x12 ^= bits.RotateLeft32(x15 + x14, 7) x13 ^= bits.RotateLeft32(x12 + x15, 9) x14 ^= bits.RotateLeft32(x13 + x12, 13) x15 ^= bits.RotateLeft32(x14 + x13, 18) } x0 += w0 x1 += w1 x2 += w2 x3 += w3 x4 += w4 x5 += w5 x6 += w6 x7 += w7 x8 += w8 x9 += w9 x10 += w10 x11 += w11 x12 += w12 x13 += w13 x14 += w14 x15 += w15 out[0], tmp[0] = x0, x0 out[1], tmp[1] = x1, x1 out[2], tmp[2] = x2, x2 out[3], tmp[3] = x3, x3 out[4], tmp[4] = x4, x4 out[5], tmp[5] = x5, x5 out[6], tmp[6] = x6, x6 out[7], tmp[7] = x7, x7 out[8], tmp[8] = x8, x8 out[9], tmp[9] = x9, x9 out[10], tmp[10] = x10, x10 out[11], tmp[11] = x11, x11 out[12], tmp[12] = x12, x12 out[13], tmp[13] = x13, x13 out[14], tmp[14] = x14, x14 out[15], tmp[15] = x15, x15 } func blockMix(tmp *[16]uint32, in []uint32, out []uint32, r int) { blockCopy(tmp[:], in[(2*r-1)*16:], 16) for i := 0; i < 2*r; i += 2 { salsaXOR(tmp, in[i*16:], out[i*8:]) salsaXOR(tmp, in[i*16+16:], out[i*8+r*16:]) } } func integer(b []uint32, r int) uint64 { j := (2*r - 1) * 16 return uint64(b[j]) | uint64(b[j+1])<<32 } func smix(b []byte, r int, N int, v []uint32, xy []uint32) { var tmp [16]uint32 R := 32 * r x := xy y := xy[R:] j := 0 for i := 0; i < R; i++ { x[i] = binary.LittleEndian.Uint32(b[j:]) j += 4 } for i := 0; i < N; i += 2 { blockCopy(v[i*R:], x, R) blockMix(&tmp, x, y, r) blockCopy(v[(i+1)*R:], y, R) blockMix(&tmp, y, x, r) } for i := 0; i < N; i += 2 { j := int(integer(x, r) & uint64(N-1)) blockXOR(x, v[j*R:], R) blockMix(&tmp, x, y, r) j = int(integer(y, r) & uint64(N-1)) blockXOR(y, v[j*R:], R) blockMix(&tmp, y, x, r) } j = 0 for _, v := range x[:R] { binary.LittleEndian.PutUint32(b[j:], v) j += 4 } } // Key derives a key from the password, salt, and cost parameters, returning // a byte slice of length keyLen that can be used as cryptographic key. // // N is a CPU/memory cost parameter, which must be a power of 2 greater than 1. // r and p must satisfy r * p < 2³⁰. If the parameters do not satisfy the // limits, the function returns a nil byte slice and an error. // // For example, you can get a derived key for e.g. AES-256 (which needs a // 32-byte key) by doing: // // dk, err := scrypt.Key([]byte("some password"), salt, 32768, 8, 1, 32) // // The recommended parameters for interactive logins as of 2017 are N=32768, r=8 // and p=1. The parameters N, r, and p should be increased as memory latency and // CPU parallelism increases; consider setting N to the highest power of 2 you // can derive within 100 milliseconds. Remember to get a good random salt. func Scrypt( password []byte, salt []byte, N int, r int, p int, keyLen int, ) ([]byte, error) { if N <= 1 || N&(N-1) != 0 { return nil, errors.New("scrypt: N must be > 1 and a power of 2") } if uint64(r)*uint64(p) >= 1<<30 || r > maxInt/128/p || r > maxInt/256 || N > maxInt/128/r { return nil, errors.New("scrypt: parameters are too large") } xy := make([]uint32, 64*r) v := make([]uint32, 32*N*r) b := PBKDF2Key(password, salt, 1, p*128*r, sha256.New) for i := 0; i < p; i++ { smix(b[i*128*r:], r, N, v, xy) } return PBKDF2Key(password, b, 1, keyLen, sha256.New), nil } // type UUID string type Channel struct { } type Connection struct { conn net.Conn replyChan chan string lastReadFrom time.Time lastWrittenTo time.Time // id *UUID id string isAuthenticated bool } type User struct { connections []Connection } type State struct { users map[string]*User } type Context struct { db *sql.DB state State tx chan int } type MessageParams struct { Middle []string Trailing string } type Message struct { Prefix string Command string Params MessageParams Raw string } var ( CmdUSER = Message { Command: "USER" } CmdPRIVMSG = Message { Command: "PRIVMSG" } CmdJOIN = Message { Command: "JOIN" } ) func SplitOnCRLF(data []byte, _atEOF bool) (int, []byte, error) { idx := bytes.Index(data, []byte { '\r', '\n' }) if idx == -1 { return 0, nil, nil } return idx + 2, data[0:idx], nil } func SplitOnRawMessage(data []byte, atEOF bool) (int, []byte, error) { advance, token, error := SplitOnCRLF(data, atEOF) if len(token) == 0 { return advance, nil, error } return advance, token, error } func SplitSpaces(r rune) bool { return r == ' ' } func ParseMessageParams(params string) MessageParams { const sep = " :" var middle string var trailing string idx := strings.Index(params, sep) if idx == -1 { middle = params trailing = "" } else { middle = params[:idx] trailing = params[idx + len(sep):] } return MessageParams { Middle: strings.FieldsFunc(middle, SplitSpaces), Trailing: trailing, } } var MessageRegex = regexp.MustCompilePOSIX( // //1 2 3 4 `^(:([^ ]+) +)?([a-zA-Z]+) *( .*)$`, ) func ParseMessage(rawMessage string) (Message, error) { var msg Message components := MessageRegex.FindStringSubmatch(rawMessage) if components == nil { return msg, errors.New("Can't parse message") } msg = Message { Prefix: components[2], Command: components[3], Params: ParseMessageParams(components[4]), Raw: rawMessage, } return msg, nil } func HandleUnknown(ctx *Context, msg Message) { Warning( "Unsupported command", "unsupported-command", "command", msg.Command, ) var r Reply = ReplyUnknown r.Prefix = "dunno" // return []Action { r } } func HandleUSER(ctx *Context, msg Message) { fmt.Printf("USER: %#v\n", msg) } func HandlePRIVMSG(ctx *Context, msg Message) { // . assert no missing params // . write to DB: (after auth) // . channel timeline: message from $USER // . reply to $USER // . broadcast new timeline event to members of the channel stmt, err := ctx.db.Prepare(` INSERT INTO messages (id, sender_id, body, timestamp) VALUES (?, ?, ?, ? ); `) if err != nil { // FIXME: reply error fmt.Println("can't prepare: ", err) return } defer stmt.Close() ret, err := stmt.Exec( NewUUID().ToString(), "FIXME", "FIXME", time.Now(), ) if err != nil { // FIXME: reply error fmt.Println("xablau can't prepare: ", err) return } fmt.Println("ret: ", ret) } func HandleJOIN(ctx *Context, msg Message) { fmt.Printf("JOIN: %#v\n", msg) // . write to DB: (after auth) // . $USER now in channel // . channel timeline: $USER joined // . reply to $USER // . broadcast new timeline event to members of the channel } func ReplyAnonymous() { } func PersistMessage(msg Message) { } type ActionType int const ( ActionReply = iota ) type Action interface { Type() ActionType } type Reply struct { Prefix string Command int Params MessageParams } func (reply Reply) Type() ActionType { return ActionReply } var ( ReplyUnknown = Reply { Command: 421, Params: MessageParams { Middle: []string { }, Trailing: "Unknown command", }, } ) var Commands = map[string]func(*Context, Message) { CmdUSER.Command: HandleUSER, CmdPRIVMSG.Command: HandlePRIVMSG, CmdJOIN.Command: HandleJOIN, } func ActionFnFor(command string) func(*Context, Message) { fn := Commands[command] if fn != nil { return fn } return HandleUnknown } func ProcessMessage(ctx *Context, connection *Connection, rawMessage string) { connection.lastReadFrom = time.Now() msg, err := ParseMessage(rawMessage) if err != nil { Info( "Error processing message", "process-message", "err", err, ) return } if msg.Command == CmdUSER.Command { args := msg.Params.Middle if len(args) == 0 { go ReplyAnonymous() return } connection.id = args[0] connection.isAuthenticated = true } if !connection.isAuthenticated { go ReplyAnonymous() return } ActionFnFor(msg.Command)(ctx, msg) } func ReadLoop(ctx *Context, connection *Connection) { scanner := bufio.NewScanner(connection.conn) scanner.Split(SplitOnRawMessage) for scanner.Scan() { ProcessMessage(ctx, connection, scanner.Text()) } } func WriteLoop(ctx *Context, connection *Connection) { for message := range connection.replyChan { _, err := io.WriteString(connection.conn, message) if err != nil { Error( "Failed to send data to user", "user-reply-error", "err", err, ) EmitWriteToClientError() continue } connection.lastWrittenTo = time.Now() } EmitActiveConnection.Dec() connection.conn.Close() } func Kill(ctx *Context, connection *Connection) { // lock? delete(ctx.state.users, connection.id) // unlock? close(connection.replyChan) connection.conn.Close() // Ignore errors? } const PingWindow = 30 * time.Second func PingLoop(ctx *Context, connection *Connection) { for { time.Sleep(PingWindow) if (time.Since(connection.lastReadFrom) <= PingWindow) { continue } window := connection.lastWrittenTo.Sub(connection.lastReadFrom) if (window <= PingWindow) { connection.replyChan <- "PING" continue } Kill(ctx, connection) break } } func HandleConnection(ctx *Context, conn net.Conn) { EmitActiveConnection.Inc() // FIXME: WaitGroup here? now := time.Now() connection := Connection { conn: conn, isAuthenticated: false, lastReadFrom: now, lastWrittenTo: now, } go ReadLoop(ctx, &connection) go WriteLoop(ctx, &connection) go PingLoop(ctx, &connection) } func IRCdLoop(ctx *Context, publicSocketPath string) { listener, err := net.Listen("unix", publicSocketPath) FatalIf(err) Info("IRCd started", "component-up", "component", "ircd") for { conn, err := listener.Accept() if err != nil { Warning( "Error accepting a public IRCd connection", "accept-connection", "err", err, ) // conn.Close() // FIXME: is conn nil? continue } // FIXME: where does it get closed go HandleConnection(ctx, conn) } } func CommandListenerLoop(ctx *Context, commandSocketPath string) { listener, err := net.Listen("unix", commandSocketPath) FatalIf(err) Info( "command listener started", "component-up", "component", "command-listener", ) for { conn, err := listener.Accept() if err != nil { Warning( "Error accepting a command connection", "accept-command", "err", err, ) continue } defer conn.Close() // TODO: handle commands } } func TransactorLoop(ctx *Context) { Info("transactor started", "component-up", "component", "transactor") EmitActiveConnection.Inc() for tx := range ctx.tx { fmt.Println(tx) } } func SetHostname() { var err error Hostname, err = os.Hostname() FatalIf(err) } func SetEnvironmentVariables() { Version = os.Getenv("PAPOD_VERSION") if Version == "" { Version = "PAPOD-VERSION-UNKNOWN" } Colour = os.Getenv("PAPOD_COLOUR") if Colour == "" { Colour = "PAPOD-COLOUR-UNKNOWN" } } func InitMigrations(db *sql.DB) { _, err := db.Exec(` CREATE TABLE IF NOT EXISTS migrations ( filename TEXT PRIMARY KEY ); `) FatalIf(err) } const MIGRATIONS_DIR = "src/sql/migrations/" func PendingMigrations(db *sql.DB) []string { files, err := ioutil.ReadDir(MIGRATIONS_DIR) FatalIf(err) set := make(map[string]bool) for _, file := range files { set[file.Name()] = true } rows, err := db.Query(`SELECT filename FROM migrations;`) FatalIf(err) defer rows.Close() for rows.Next() { var filename string err := rows.Scan(&filename) FatalIf(err) delete(set, filename) } FatalIf(rows.Err()) difference := make([]string, 0) for filename := range set { difference = append(difference, filename) } sort.Sort(sort.StringSlice(difference)) return difference } func RunMigrations(db *sql.DB) { InitMigrations(db) stmt, err := db.Prepare(`INSERT INTO migrations (filename) VALUES (?);`) FatalIf(err) defer stmt.Close() for _, filename := range PendingMigrations(db) { Info("Running migration file", "exec-migration-file", "filename", filename, ) tx, err := db.Begin() FatalIf(err) sql, err := os.ReadFile(MIGRATIONS_DIR + filename) FatalIf(err) _, err = tx.Exec(string(sql)) FatalIf(err) _, err = tx.Stmt(stmt).Exec(filename) FatalIf(err) err = tx.Commit() FatalIf(err) } } func InitDB(databasePath string) *sql.DB { db, err := sql.Open("sqlite3", databasePath) FatalIf(err) RunMigrations(db) return db } func Init() { SetLoggerOutput(os.Stdout) SetTraceback() SetHostname() SetEnvironmentVariables() } func Start(ctx *Context, publicSocketPath string, commandSocketPath string) { buildInfo, ok := debug.ReadBuildInfo() if !ok { Fatal(errors.New("error on debug.ReadBuildInfo()")) } Info("-", "lifecycle-event", "event", "starting-server", slog.Group( "go", "version", buildInfo.GoVersion, "settings", buildInfo.Settings, "deps", buildInfo.Deps, ), ) var wg sync.WaitGroup bgRun := func(f func()) { wg.Add(1) go func() { f() wg.Done() }() } bgRun(func() { IRCdLoop(ctx, publicSocketPath) }) bgRun(func() { CommandListenerLoop(ctx, commandSocketPath) }) bgRun(func() { TransactorLoop(ctx) }) wg.Wait() } func BuildContext(databasePath string) *Context { db := InitDB(databasePath) tx := make(chan int, 100) return &Context { db: db, tx: tx, } } var ( databasePath = flag.String( "f", "papod.db", "The path to the database file", ) publicSocketPath = flag.String( "s", "papod.public.socket", "The path to the socket that handles the public traffic", ) commandSocketPath = flag.String( "S", "papod.command.socket", "The path to the private IPC commands socket", ) ) func Main() { Init() flag.Parse() ctx := BuildContext(*databasePath) Start(ctx, *publicSocketPath, *commandSocketPath) }