diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/main.go | 7 | ||||
| -rw-r--r-- | src/papod.go | 1158 |
2 files changed, 0 insertions, 1165 deletions
diff --git a/src/main.go b/src/main.go deleted file mode 100644 index b591f5c..0000000 --- a/src/main.go +++ /dev/null @@ -1,7 +0,0 @@ -package main - -import "papod" - -func main() { - papod.Main() -} diff --git a/src/papod.go b/src/papod.go deleted file mode 100644 index 7db55e8..0000000 --- a/src/papod.go +++ /dev/null @@ -1,1158 +0,0 @@ -package papod - -import ( - "bufio" - "bytes" - "errors" - "flag" - "fmt" - "io" - "log/slog" - "net" - "os" - "regexp" - "strings" - "sync" - - "acude" - "cracha" - "uuid" - "pds" - "stm" - g "gobang" - gt "gotext" -) - - - -type newUserT struct{ -} - -type userT struct{ - username string -} - -type newNetworkT struct{ -} - -type networkT struct{ -} - -type newMemberT struct{ -} - -type memberT struct{ -} - -type newChannelT struct{ -} - -type channelT struct{ -} - -type newEventT struct{ -} - -type eventT struct{ -} - -type queriesT struct{ - createUser func(newUserT) (userT, error) - userByUUID func(uuid.UUID) (userT, error) - editUser func(userT) error - removeUser func(userT) error - createNetwork func(userT, newNetworkT) (networkT, error) - networkByUUID func(userT, uuid.UUID) (networkT, error) - allNetworks func(userT) (<-chan networkT, error) - editNetwork func(memberT, networkT) error - removeNetwork func(memberT) error - addMember func(memberT, newMemberT) (memberT, error) - addRoles func(memberT, []string, memberT) error - removeRoles func(memberT, []string, memberT) error - memberByUUID func(memberT, uuid.UUID) (memberT, error) - allMembers func(memberT) (<-chan memberT, error) - editMember func(memberT, memberT) error - removeMember func(memberT, memberT) error - createChannel func(memberT, newChannelT) (channelT, error) - channelsByName func(memberT, string) (<-chan channelT) - editChannel func(memberT, channelT) error - removeChannel func(memberT, channelT) error - joinChannel func(memberT, channelT) error - partChannel func(memberT, channelT) error - addEvent func(memberT, newEventT) (eventT, error) - eventsAfter func(memberT, eventT) (<-chan eventT, error) - editEvent func(memberT, eventT) (eventT, error) - removeEvent func(memberT, uuid.UUID) error - close func() error -} - -type messageT struct{ - prefix string - command string - params []string - raw string -} - -type replyT struct{ - command string - params []string -} - -type taskT struct{ -} - -type actionT struct{ - replies []replyT - shouldClose bool - err error - task []taskT -} - -type listenersT struct{ - daemon net.Listener - commander net.Listener - close func() error -} - -type netConnI interface{ - Write(p []byte) (n int, err error) - Close() error -} - -type connectionT struct{ - uuid uuid.UUID - user *userT - conn netConnI - send func(messageT) -} - -type metricsT struct{ - activeConnections g.Gauge - nicksInChannel g.Gauge - sendToClientError func(...any) - receivedMessage func(...any) - sentReply func(...any) -} - -type stateT struct{ - members *pds.Map[string, []string] - users *pds.Map[string, []uuid.UUID] - connections *pds.Map[uuid.UUID, connectionT] -} - -type papodT struct{ - acude acude.AcudeI - cracha cracha.CrachaI - listeners listenersT - state *stm.AtomT[stateT] - metrics metricsT -} - -type PapoI interface{ - Start() error - Close() error -} - -type argsT struct{ - allArgs []string - subArgs []string - baseDir string - tag string -} - -type envT struct{ - args argsT - in io.Reader - out io.Writer - err io.Writer -} - - - -func initListeners( - daemonSocketPath string, - commanderSocketPath string, -) (listenersT, error) { - _ = os.Remove(daemonSocketPath) - _ = os.Remove(commanderSocketPath) - - daemon, err := net.Listen("unix", daemonSocketPath) - if err != nil { - return listenersT{}, err - } - - commander, err := net.Listen("unix", commanderSocketPath) - if err != nil { - daemon.Close() - return listenersT{}, err - } - - return listenersT{ - daemon: daemon, - commander: commander, - close: func() error { - return g.SomeFnError( - daemon.Close, - commander.Close, - ) - }, - }, nil -} - -func newState() stateT { - return stateT{ - members: pds.NewMap[string, []string](nil), - users: pds.NewMap[string, []uuid.UUID](nil), - connections: pds.NewMap[uuid.UUID, connectionT](nil), - } -} - -func buildMetrics(tag string) metricsT { - return metricsT{ - activeConnections: g.MakeGauge( - "active-connection", - "tag", tag, - ), - nicksInChannel: g.MakeGauge( - "nicks-in-channel", - "tag", tag, - ), - sendToClientError: g.MakeCounter( - "send-to-client-error", - "tag", tag, - ), - receivedMessage: g.MakeCounter( - "received-message", - "tag", tag, - ), - sentReply: g.MakeCounter( - "sent-reply", - "tag", tag, - ), - } -} - -func initDB(path string) (acude.AcudeI, error) { - return nil, nil -} - -func newPapod(baseDir string, tag string) (papodT, error) { - databasePath := baseDir + "/papod.dedo" - daemonSocketPath := baseDir + "/papod.daemon.socket" - commanderSocketPath := baseDir + "/papod.commander.socket" - - cracha, err := cracha.New(databasePath) - if err != nil { - return papodT{}, err - } - - listeners, err := initListeners(daemonSocketPath, commanderSocketPath) - if err != nil { - cracha.Close() - return papodT{}, err - } - - acude, err := initDB(databasePath) - if err != nil { - cracha.Close() - listeners.close() - return papodT{}, err - } - - return papodT{ - acude: acude, - cracha: cracha, - listeners: listeners, - state: stm.Atom(newState()), - metrics: buildMetrics(tag), - }, nil -} - -func NewWith(baseDir string, tag string) (PapoI, error) { - return newPapod(baseDir, tag) -} - -func New() (PapoI, error) { - return NewWith(".", "papod-default") -} - -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 splitCommas(r rune) bool { - return r == ',' -} - -func splitSpaces(r rune) bool { - return r == ' ' -} - -func parseMessageParams(params string) []string { - const sep = " :" - - idx := strings.Index(params, sep) - if idx == -1 { - return strings.FieldsFunc(params, splitSpaces) - } else { - middle := params[:idx] - trailing := params[idx + len(sep):] - return append( - strings.FieldsFunc(middle, splitSpaces), - trailing, - ) - } -} - -func stripBlankParams(params []string) []string { - if len(params) == 1 && len(params[0]) == 0 { - return []string{} - } - - return params -} - -var messageRegex = regexp.MustCompilePOSIX( - // <prefix> <command> <params> - //1 2 3 4 - `^(:([^ ]+) +)?([a-zA-Z]+) *( .*)$`, -) -func parseMessage(rawMessage string) (messageT, error) { - var msg messageT - - components := messageRegex.FindStringSubmatch(rawMessage) - if components == nil { - return msg, errors.New("Can't parse message") - } - - msg = messageT{ - prefix: components[2], - command: components[3], - params: stripBlankParams(parseMessageParams(components[4])), - raw: rawMessage, - } - return msg, nil -} - -func removeConnection(state stateT, connection *connectionT) stateT { - return state // FIXME -} - -/// Is this death by a thousand goroutines? Is the runtime able to handle the -/// creation and destruction of hundreds of thousands of goroutines per second? -/// For now, we'll assume that Go's (gc) runtime, scheduler and garbage -/// collector are capable of working together to make sure this isn't a -/// catastrophe. -func broadcastMessage( - message messageT, - channelName string, - state stateT, -) { - usernames, _ := state.members.Get(channelName) - for _, username := range usernames { - connectionIDs, _ := state.users.Get(username) - for _, connectionID := range connectionIDs { - connection, ok := state.connections.Get(connectionID) - if !ok { - continue - } - - go connection.send(message) - } - } -} - - -/* -Intentionally not implemented: - -// FIXME: why? -- RPL_BOUNCE - -*/ - -// FIXME: add check for minRPL... -const minRPL_WELCOME = 0 -func _RPL_WELCOME(connection *connectionT, msg messageT) replyT { - return replyT{ - command: "001", - params: []string{ - connection.user.username, - "Welcome to the Internet Relay Network " + - connection.user.username, - }, - } -} - -const minRPL_YOURHOST = 0 -func _RPL_YOURHOST(connection *connectionT, msg messageT) replyT { - return replyT{ - command: "002", - params: []string{ - connection.user.username, - "Your host is FIXME, running version " + - Version, - }, - } -} - -const minRPL_CREATED = 0 -func _RPL_CREATED(connection *connectionT, msg messageT) replyT { - return replyT{ - command: "003", - params: []string{ - connection.user.username, - "This server was create FIXME", - }, - } -} - -const minRPL_MYINFO = 0 -func _RPL_MYINFO(connection *connectionT, msg messageT) replyT { - return replyT{ - command: "004", - params: []string{ - connection.user.username, - "FIXME " + Version + " i x", - }, - } -} - -const minRPL_UNAWAY = 0 -func _RPL_UNAWAY(connection *connectionT, msg messageT) replyT { - return replyT{ - command: "305", - params: []string{ - connection.user.username, - "You are no longer marked as away", - }, - } -} - -const minRPL_NOWAWAY = 0 -func _RPL_NOWAWAY(connection *connectionT, msg messageT) replyT { - return replyT{ - command: "306", - params: []string{ - connection.user.username, - "You have been marked as away", - }, - } -} - -const minRPL_WHOISUSER = 1 -func _RPL_WHOISUSER(connection *connectionT, msg messageT) replyT { - user := msg.params[0] - return replyT{ - command: "311", - params: []string{ - connection.user.username, - user, - user, - "samehost", - "*", - "my real name is: " + user, - }, - } -} - -const minRPL_WHOISSERVER = 1 -func _RPL_WHOISSERVER(connection *connectionT, msg messageT) replyT { - user := msg.params[0] - return replyT{ - command: "312", - params: []string{ - connection.user.username, - user, - "stillsamehost", - "some server info", - }, - } -} - -const minRPL_ENDOFWHOIS = 1 -func _RPL_ENDOFWHOIS(connection *connectionT, msg messageT) replyT { - user := msg.params[0] - return replyT{ - command: "318", - params: []string{ - connection.user.username, - user, - "End of WHOIS list", - }, - } -} - -const minRPL_WHOISCHANNELS = 1 -func _RPL_WHOISCHANNELS(connection *connectionT, msg messageT) replyT { - user := msg.params[0] - return replyT{ - command: "319", - params: []string{ - connection.user.username, - user, - "#default", - }, - } -} - -const minRPL_CHANNELMODEIS = 1 -func _RPL_CHANNELMODEIS(connection *connectionT, msg messageT) replyT { - channel := msg.params[0] - return replyT{ - command: "324", - params: []string{ - connection.user.username, - channel, - "+Cnst", - }, - } -} - -const minRPL_NOTOPIC = 1 -func _RPL_NOTOPIC(connection *connectionT, msg messageT) replyT { - channel := msg.params[0] - return replyT{ - command: "331", - params: []string{ - connection.user.username, - channel, - "No topic is set", - }, - } -} - -const minRPL_NAMREPLY = 1 -func _RPL_NAMREPLY(connection *connectionT, msg messageT) replyT { - channel := msg.params[0] - return replyT{ - command: "353", - params: []string{ - connection.user.username, - "=", - channel, - connection.user.username + " virtualuser", - }, - } -} - -const minRPL_ENDOFNAMES = 1 -func _RPL_ENDOFNAMES(connection *connectionT, msg messageT) replyT { - channel := msg.params[0] - return replyT{ - command: "366", - params: []string{ - connection.user.username, - channel, - "End of NAMES list", - }, - } -} - -const minERR_UNKNOWNCOMMAND = 0 -func _ERR_UNKNOWNCOMMAND(connection *connectionT, msg messageT) replyT { - return replyT{ - command: "421", - params: []string{ - connection.user.username, - "Unknown command", - }, - } -} - -const minERR_FILEERROR = 0 -func _ERR_FILEERROR(connection *connectionT, msg messageT) replyT { - return replyT{ - command: "424", - params: []string{ - "File error doing query on database", - }, - } -} - -const minERR_NOTREGISTERED = 0 -func _ERR_NOTREGISTERED(connection *connectionT, msg messageT) replyT { - return replyT{ - command: "451", - params: []string{ - "You have not registered", - }, - } -} - -const minERR_NEEDMOREPARAMS = 0 -func _ERR_NEEDMOREPARAMS(connection *connectionT, msg messageT) replyT { - return replyT{ - command: "461", - params: []string{ - msg.command, - "Not enough parameters", - }, - } -} - - -func _CAP(connection *connectionT, msg messageT) replyT { - return replyT{ - command: "CAP", - params: []string { - "*", - "LS", - }, - } -} - -const minPONG = 0 -func _PONG(connection *connectionT, msg messageT) replyT { - return replyT{ - command: "PONG", - params: msg.params, - } -} - - -const minUSER = 4 -func handleUSER( - papod papodT, - connection *connectionT, - msg messageT, -) ([]replyT, bool, error) { - return []replyT{ - _RPL_WELCOME (connection, msg), - _RPL_YOURHOST(connection, msg), - _RPL_CREATED (connection, msg), - _RPL_MYINFO (connection, msg), - }, false, nil -} - -const minNICK = 1 -func handleNICK( - papod papodT, - connection *connectionT, - msg messageT, -) ([]replyT, bool, error) { - connection.user.username = msg.params[0] - return []replyT{}, false, nil -} - -const minPRIVMSG = 2 -func handlePRIVMSG( - papod papodT, - connection *connectionT, - msg messageT, -) ([]replyT, bool, error) { - // FIXME: check if user is member of channel, and is authorized to post - // FIXME: adapt to handle multiple targets - - go broadcastMessage( - msg, - msg.params[0], - papod.state.Deref(), - ) - return []replyT{}, false, nil -} - -const minTOPIC = 2 -func handleTOPIC( - papod papodT, - connection *connectionT, - msg messageT, -) ([]replyT, bool, error) { - return []replyT{ - _RPL_NOTOPIC(connection, msg), - }, false, nil -} - -const minJOIN = 1 -func handleJOIN( - papod papodT, - connection *connectionT, - msg messageT, -) ([]replyT, bool, error) { - // FIXME: add to database - // channels := strings.FieldsFunc(msg.params[0], splitCommas) - // papod.stateMutable.subscribe(connection.user.username, channels) - - return []replyT{ - _RPL_NOTOPIC (connection, msg), - _RPL_NAMREPLY (connection, msg), - _RPL_ENDOFNAMES(connection, msg), - }, false, nil -} - -const minMODE = 1 -func handleMODE( - papod papodT, - connection *connectionT, - msg messageT, -) ([]replyT, bool, error) { - return []replyT{ - _RPL_CHANNELMODEIS(connection, msg), - }, false, nil -} - -const minWHOIS = 1 -func handleWHOIS( - papod papodT, - connection *connectionT, - msg messageT, -) ([]replyT, bool, error) { - return []replyT{ - _RPL_WHOISUSER (connection, msg), - _RPL_WHOISSERVER (connection, msg), - _RPL_WHOISCHANNELS(connection, msg), - _RPL_ENDOFWHOIS (connection, msg), - }, false, nil -} - -const minAWAY = 0 -func handleAWAY( - papod papodT, - connection *connectionT, - msg messageT, -) ([]replyT, bool, error) { - replyFn := _RPL_NOWAWAY - if len(msg.params) == 0 { - replyFn = _RPL_UNAWAY - } - - return []replyT{ - replyFn(connection, msg), - }, false, nil -} - -const minPING = 0 -func handlePING( - papod papodT, - connection *connectionT, - msg messageT, -) ([]replyT, bool, error) { - return []replyT{ - _PONG(connection, msg), - }, false, nil -} - -const minQUIT = 0 -func handleQUIT( - papod papodT, - connection *connectionT, - msg messageT, -) ([]replyT, bool, error) { - return []replyT{}, true, nil -} - -const minCAP = 1 -func handleCAP( - papod papodT, - connection *connectionT, - msg messageT, -) ([]replyT, bool, error) { - if msg.params[0] == "END" { - return nil, false, nil - } - - return []replyT{ - _CAP(connection, msg), - }, false, nil -} - - -func authRequired( - fn func( - papodT, - *connectionT, - messageT, - ) ([]replyT, bool, error), -) func(papodT, *connectionT, messageT) ([]replyT, bool, error) { - return func( - papod papodT, - connection *connectionT, - msg messageT, - ) ([]replyT, bool, error) { - if connection.user == nil { - return []replyT{ - _ERR_NOTREGISTERED(connection, msg), - }, false, nil - } - - return fn(papod, connection, msg) - } -} - -func minArgs( - count int, - fn func( - papodT, - *connectionT, - messageT, - ) ([]replyT, bool, error), -) func(papodT, *connectionT, messageT) ([]replyT, bool, error) { - return func( - papod papodT, - connection *connectionT, - msg messageT, - ) ([]replyT, bool, error) { - if len(msg.params) < count { - return []replyT{ - _ERR_NEEDMOREPARAMS(connection, msg), - }, false, nil - } - - return fn(papod, connection, msg) - } -} - -var commands = map[string]func( - papodT, - *connectionT, - messageT, -) ([]replyT, bool, error) { - "USER": minArgs(minUSER, handleUSER), - "NICK": minArgs(minNICK, handleNICK), - "QUIT": minArgs(minQUIT, handleQUIT), - "CAP": minArgs(minCAP, handleCAP), - "AWAY": authRequired(minArgs(minAWAY, handleAWAY)), - "PRIVMSG": authRequired(minArgs(minPRIVMSG, handlePRIVMSG)), - "PING": authRequired(minArgs(minPING, handlePING)), - "JOIN": authRequired(minArgs(minJOIN, handleJOIN)), - "MODE": authRequired(minArgs(minMODE, handleMODE)), - "TOPIC": authRequired(minArgs(minTOPIC, handleTOPIC)), - "WHOIS": authRequired(minArgs(minWHOIS, handleWHOIS)), -} - -func handleUnknown( - papod papodT, - connection *connectionT, - msg messageT, -) ([]replyT, bool, error) { - // FIXME: user doesn't exist when unauthenticated - /* - err := papod.queries.logMessage(userT{ }, msg) - if err != nil { - g.Warning( - "Failed to log message", fmt.Sprintf("%#v", msg), - "group-as", "db-write", - "handler-action", "log-and-ignore", - "connection", connection.uuid.String(), - "err", err, - ) - } - */ - - return []replyT{ - _ERR_UNKNOWNCOMMAND(connection, msg), - }, false, nil -} - -func actionFnFor( - command string, -) func(papodT, *connectionT, messageT) ([]replyT, bool, error) { - fn := commands[command] - if fn != nil { - return fn - } - - return handleUnknown -} - -func addTrailingSeparator(strs []string) { - if len(strs) == 0 { - return - } - - last := strs[len(strs) - 1] - if strings.Contains(last, " ") && last[0] != ':' { - strs[len(strs) - 1] = ":" + last - } -} - -func (r replyT) String() string { - addTrailingSeparator(r.params) - return fmt.Sprintf( - "%s %s\r\n", - r.command, - strings.Join(r.params, " "), - ) -} - -func (m messageT) logAttributes() slog.Attr { - return slog.Group( - "message", - "prefix", m.prefix, - "raw", m.raw, - "params", m.params, - ) -} - -func (r replyT) logAttributes() slog.Attr { - return slog.Group( - "reply", - "command", r.command, - "params", r.params, - ) -} - -func processMessage( - papod papodT, - connection *connectionT, - rawMessage string, -) { - msg, err := parseMessage(rawMessage) - if err != nil { - g.Info( - "Error parsing message", "parse-message-error", - slog.Group( - "message", - "text", rawMessage, - ), - "err", err, - ) - return - } - papod.metrics.receivedMessage(msg.logAttributes()) - - var replyErrors []error - replies, shouldClose, actionErr := actionFnFor(msg.command)( - papod, - connection, - msg, - ) - for _, reply := range replies { - _, err = io.WriteString(connection.conn, reply.String()) - if err != nil { - replyErrors = append(replyErrors, err) - } - - papod.metrics.sentReply( - msg.logAttributes(), - reply.logAttributes(), - ) - } - - if actionErr != nil || len(replyErrors) != 0 { - if actionErr != nil { - g.Info( - "Handler returned error", "handler-error", - "from", "daemon", - "err", err, - ) - } - - if len(replyErrors) != 0 { - g.Info( - "Failed to send reply", "send-reply-error", - "from", "daemon", - "err", replyErrors, - ) - } - - stm.Swap(papod.state, func(state stateT) stateT { - return removeConnection(state, connection) - }) - err := connection.conn.Close() - if err != nil { - g.Warning( - "Failed to close the connection", - "close-error", - "from", "daemon", - "err", err, - ) - } - - return - } - - if shouldClose { - // FIXME - // papod.stateMutable.disconnect(connection) - } -} - -func processTasks() // FIXME - -func handleConnection(papod papodT, conn net.Conn) { - connection := connectionT{ - uuid: uuid.New(), - user: &userT{}, // TODO: SASL shenanigan probably goes here - conn: conn, - } - scanner := bufio.NewScanner(conn) - scanner.Split(splitOnRawMessage) - for scanner.Scan() { - processMessage(papod, &connection, scanner.Text()) - } -} - -func daemonLoop(papod papodT) { - for { - conn, err := papod.listeners.daemon.Accept() - if err != nil { - if errors.Is(err, net.ErrClosed) { - break - } - - g.Warning( - "Error accepting daemon connection", - "accept-connection", - "from", "daemon", - "err", err, - ) - continue - } - go handleConnection(papod, conn) - } -} - -func commanderLoop(papod papodT) { - for { - conn, err := papod.listeners.commander.Accept() - if err != nil { - if errors.Is(err, net.ErrClosed) { - break - } - - g.Warning( - "Error accepting commander connection", - "accept-connection", - "from", "commander", - "err", err, - ) - continue - } - go handleConnection(papod, conn) - } -} - -func mkbgrun() (func(func()), func()) { - var wg sync.WaitGroup - bgrun := func(f func()) { - wg.Add(1) - go func() { - f() - wg.Done() - }() - } - return bgrun, wg.Wait -} - -func (papod papodT) Start() error { - g.Info("Starting service", "lifecycle-event", - "event", "starting-server", - slog.Group( - "versions", - "cracha", cracha.Version, - "uuid", uuid.Version, - "pds", pds.Version, - "stm", stm.Version, - "papod", Version, - "gobang", g.Version, - "gotext", gt.Version, - ), - ) - - run, wait := mkbgrun() - run(func() { daemonLoop(papod) }) - run(func() { commanderLoop(papod) }) - wait() - - return nil -} - -func (papod papodT) Close() error { - // FIXME: does this wait for current handlers to wait? Well, it should. - /* - return g.WrapErrors( - papod.listeners.close(), - // papod.connCloser.closeAll(), - papod.auth.Close(), - papod.queue.Close(), - papod.queries.close(), - ) - */ - return nil -} - -func usage(argv0 string, w io.Writer) { - fmt.Fprintf( - w, - gt.Gettext("Usage: %s [-t TAG] [BASEDIR]"), - argv0, - ) -} - -func getopt(allArgs []string, w io.Writer) (argsT, int) { - argv0 := allArgs[0] - argv := allArgs[1:] - fs := flag.NewFlagSet("", flag.ContinueOnError) - fs.Usage = func() {} - fs.SetOutput(w) - - tag := fs.String( - "t", - "", - "The specific instance tag for inclusion in the log", - ) - if fs.Parse(argv) != nil { - usage(argv0, w) - return argsT{}, 2 - } - - subArgs := fs.Args() - baseDir := "." - if len(subArgs) != 0 { - baseDir = subArgs[0] - } - - return argsT{ - allArgs: allArgs, - subArgs: subArgs, - baseDir: baseDir, - tag: *tag, - }, 0 -} - -func run(env envT) int { - papod, err := newPapod(env.args.baseDir, env.args.tag) - if err != nil { - fmt.Fprintln(env.err, err) - return 1 - } - - err = papod.Start() - if err != nil { - fmt.Fprintln(env.err, err) - return 1 - } - - return 0 -} - - - -func Main() { - g.Init() - gt.Init(Name, LOCALEDIR) - args, rc := getopt(os.Args, os.Stderr) - g.ExitIf(rc) - os.Exit(run(envT{ - args: args, - in: os.Stdin, - out: os.Stdout, - err: os.Stderr, - })) -} |
