diff options
| author | Přemysl Janouch <p@janouch.name> | 2018-07-29 07:50:27 +0200 | 
|---|---|---|
| committer | Přemysl Janouch <p@janouch.name> | 2018-07-29 08:14:07 +0200 | 
| commit | 2dfb4e45d1c8f073ec0a5a9a4761acbebeb3fc80 (patch) | |
| tree | 1bb7d9fb5d68bb02dae04a12565f0b1065891196 /hid | |
| parent | f5def2e579f6c9f1f10fe84ac7435fad4c928116 (diff) | |
| download | haven-2dfb4e45d1c8f073ec0a5a9a4761acbebeb3fc80.tar.gz haven-2dfb4e45d1c8f073ec0a5a9a4761acbebeb3fc80.tar.xz haven-2dfb4e45d1c8f073ec0a5a9a4761acbebeb3fc80.zip | |
hid: first round of mixed fixes and cleanups
Diffstat (limited to 'hid')
| -rw-r--r-- | hid/main.go | 460 | 
1 files changed, 253 insertions, 207 deletions
| diff --git a/hid/main.go b/hid/main.go index 72a6df0..98a8b3b 100644 --- a/hid/main.go +++ b/hid/main.go @@ -16,55 +16,101 @@  // hid is a straight-forward port of kike IRCd from C.  package main -/* +import ( +	"bufio" +	"crypto/sha256" +	"crypto/tls" +	"encoding/hex" +	"flag" +	"fmt" +	"io" +	"log" +	"net" +	"os" +	"os/signal" +	"os/user" +	"path/filepath" +	"regexp" +	"strconv" +	"strings" +	"syscall" +	"time" +) -// ANSI terminal formatting, would be better if we had isatty() available -func tf(text string, ansi string) string { -	return "\x1b[0;" + ansi + "m" + text + "\x1b[0m" -} +var debugMode = false -func logErrorf(format string, args ...interface{}) { -	fmt.Fprintf(os.Stderr, tf("error: "+format+"\n", "1;31"), args...) -} +const ( +	projectName = "hid" +	// TODO: Consider using the same version number for all subprojects. +	projectVersion = "0" +) -func logFatalf(format string, args ...interface{}) { -	fmt.Fprintf(os.Stderr, tf("fatal: "+format+"\n", "1;31"), args...) -	os.Exit(1) -} +// --- Utilities --------------------------------------------------------------- -func logFatal(object interface{}) { -	logFatalf("%s", object) +// Split a string by a set of UTF-8 delimiters, optionally ignoring empty items. +func splitString(s, delims string, ignoreEmpty bool) (result []string) { +	for { +		end := strings.IndexAny(s, delims) +		if end < 0 { +			break +		} +		if !ignoreEmpty || end != 0 { +			result = append(result, s[:end]) +		} +		s = s[end+1:] +	} +	if !ignoreEmpty || s != "" { +		result = append(result, s) +	} +	return  } -func getHome() (home string) { -	if u, _ := user.Current(); u != nil { -		home = u.HomeDir -	} else { -		home = os.Getenv("HOME") +func findTildeHome(username string) string { +	if username != "" { +		if u, _ := user.Lookup(username); u != nil { +			return u.HomeDir +		} +	} else if u, _ := user.Current(); u != nil { +		return u.HomeDir +	} else if v, ok := os.LookupEnv("HOME"); ok { +		return v  	} -	return +	return "~" + username  } -// Only handling the simple case as that's what one mostly wants. -// TODO(p): Handle the generic case as well. +// Tries to expand the tilde in paths, leaving it as-is on error.  func expandTilde(path string) string { -	if strings.HasPrefix(path, "~/") { -		return getHome() + path[1:] +	if path[0] != '~' { +		return path  	} -	return path + +	var n int +	for n = 0; n < len(path); n++ { +		if path[n] == '/' { +			break +		} +	} +	return findTildeHome(path[1:n]) + path[n:]  } -func getXdgHomeDir(name, def string) string { +func getXDGHomeDir(name, def string) string {  	env := os.Getenv(name)  	if env != "" && env[0] == '/' {  		return env  	} -	return filepath.Join(getHome(), def) + +	home := "" +	if v, ok := os.LookupEnv("HOME"); ok { +		home = v +	} else if u, _ := user.Current(); u != nil { +		home = u.HomeDir +	} +	return filepath.Join(home, def)  } -// Retrieve all XDG base directories for configuration files -func getXdgConfigDirs() (result []string) { -	home := getXdgHomeDir("XDG_CONFIG_HOME", ".config") +// Retrieve all XDG base directories for configuration files. +func getXDGConfigDirs() (result []string) { +	home := getXDGHomeDir("XDG_CONFIG_HOME", ".config")  	if home != "" {  		result = append(result, home)  	} @@ -80,56 +126,6 @@ func getXdgConfigDirs() (result []string) {  	return  } -// Read a configuration file with the given basename w/o extension -func readConfigFile(name string, output interface{}) error { -	var suffix = filepath.Join(projectName, name+".json") -	for _, path := range getXdgConfigDirs() { -		full := filepath.Join(path, suffix) -		file, err := os.Open(full) -		if err != nil { -			if !os.IsNotExist(err) { -				return err -			} -			continue -		} -		defer file.Close() - -		decoder := json.NewDecoder(file) -		err = decoder.Decode(output) -		if err != nil { -			return fmt.Errorf("%s: %s", full, err) -		} -		return nil -	} -	return errors.New("configuration file not found") -} - -*/ - -import ( -	"bufio" -	"crypto/sha256" -	"crypto/tls" -	"encoding/hex" -	"flag" -	"fmt" -	"io" -	"log" -	"net" -	"os" -	"os/signal" -	"path/filepath" -	"regexp" -	"strconv" -	"strings" -	"syscall" -	"time" -) - -var debugMode = false - -// --- Utilities --------------------------------------------------------------- -  //  // Trivial SSL/TLS autodetection. The first block of data returned by Recvfrom  // must be at least three octets long for this to work reliably, but that should @@ -173,6 +169,35 @@ var config = []struct {  	{"bind", []rune(":6667"), "Address of the IRC server"},  } +/* + +// Read a configuration file with the given basename w/o extension. +func readConfigFile(name string, output interface{}) error { +	var suffix = filepath.Join(projectName, name+".json") +	for _, path := range getXDGConfigDirs() { +		full := filepath.Join(path, suffix) +		file, err := os.Open(full) +		if err != nil { +			if !os.IsNotExist(err) { +				return err +			} +			continue +		} +		defer file.Close() + +		// TODO: We don't want to use JSON. +		decoder := json.NewDecoder(file) +		err = decoder.Decode(output) +		if err != nil { +			return fmt.Errorf("%s: %s", full, err) +		} +		return nil +	} +	return errors.New("configuration file not found") +} + +*/ +  // --- Rate limiter ------------------------------------------------------------  type floodDetector struct { @@ -231,21 +256,40 @@ func ircToLower(c byte) byte {  	return c  } -// TODO: To support ALL CAPS initialization of maps, perhaps we should use -// ircToUpper instead. -// FIXME: This doesn't follow the meaning of strxfrm and perhaps should be -// renamed to ircNormalize. -func ircStrxfrm(ident string) string { +func ircToUpper(c byte) byte { +	switch c { +	case '{': +		return '[' +	case '}': +		return ']' +	case '|': +		return '\\' +	case '^': +		return '~' +	} +	if c >= 'a' && c <= 'z' { +		return c - ('a' - 'A') +	} +	return c +} + +// Convert identifier to a canonical form for case-insensitive comparisons. +// ircToUpper is used so that statically initialized maps can be in uppercase. +func ircToCanon(ident string) string {  	var canon []byte  	for _, c := range []byte(ident) { -		canon = append(canon, ircToLower(c)) +		canon = append(canon, ircToUpper(c))  	}  	return string(canon)  } +func ircEqual(s1, s2 string) bool { +	return ircToCanon(s1) == ircToCanon(s2) +} +  func ircFnmatch(pattern string, s string) bool { -	pattern, s = ircStrxfrm(pattern), ircStrxfrm(s) -	// FIXME: This should not support [] ranges and handle / specially. +	pattern, s = ircToCanon(pattern), ircToCanon(s) +	// FIXME: This should not support [] ranges and handle '/' specially.  	// We could translate the pattern to a regular expression.  	matched, _ := filepath.Match(pattern, s)  	return matched @@ -331,8 +375,8 @@ type client struct {  	transport net.Conn       // underlying connection  	tls       *tls.Conn      // TLS, if detected  	conn      connCloseWrite // high-level connection -	inQ       []byte         // unprocessed input -	outQ      []byte         // unprocessed output +	recvQ     []byte         // unprocessed input +	sendQ     []byte         // unprocessed output  	reading   bool           // whether a reading goroutine is running  	writing   bool           // whether a writing goroutine is running  	closing   bool           // whether we're closing the connection @@ -459,10 +503,8 @@ type writeEvent struct {  var (  	started int64 // when has the server been started -	users       map[string]*client  // maps nicknames to clients -	channels    map[string]*channel // maps channel names to data -	handlers    map[string]bool     // TODO message handlers -	capHandlers map[string]bool     // TODO CAP message handlers +	users    map[string]*client  // maps nicknames to clients +	channels map[string]*channel // maps channel names to data  	whowas map[string]*whowasInfo // WHOWAS registry @@ -481,38 +523,38 @@ var (  	writes   = make(chan writeEvent)  	timeouts = make(chan *client) -	tlsConf  *tls.Config -	clients  = make(map[*client]bool) -	listener net.Listener -	// TODO: quitting, quitTimer as they are named in kike? -	inShutdown    bool -	shutdownTimer <-chan time.Time +	tlsConf   *tls.Config +	clients   = make(map[*client]bool) +	listener  net.Listener +	quitting  bool +	quitTimer <-chan time.Time  )  // Forcefully tear down all connections. -func forceShutdown(reason string) { -	if !inShutdown { -		log.Fatalln("forceShutdown called without initiateShutdown") +func forceQuit(reason string) { +	if !quitting { +		log.Fatalln("forceQuit called without initiateQuit")  	}  	log.Printf("forced shutdown (%s)\n", reason)  	for c := range clients { -		c.destroy("TODO") +		// initiateQuit has already unregistered the client. +		c.kill("Shutting down")  	}  }  // Initiate a clean shutdown of the whole daemon. -func initiateShutdown() { +func initiateQuit() {  	log.Println("shutting down")  	if err := listener.Close(); err != nil {  		log.Println(err)  	}  	for c := range clients { -		c.closeLink("TODO") +		c.closeLink("Shutting down")  	} -	shutdownTimer = time.After(5 * time.Second) -	inShutdown = true +	quitTimer = time.After(5 * time.Second) +	quitting = true  }  // TODO: ircChannelCreate @@ -538,32 +580,31 @@ func ircSendToRoommates(c *client, message string) {  // --- Clients (continued) ----------------------------------------------------- -// TODO: Perhaps we should append to *[]byte for performance. -func clientModeToString(m uint, mode *string) { +func clientModeToString(m uint, mode *[]byte) {  	if 0 != m&ircUserModeInvisible { -		*mode += "i" +		*mode = append(*mode, 'i')  	}  	if 0 != m&ircUserModeRxWallops { -		*mode += "w" +		*mode = append(*mode, 'w')  	}  	if 0 != m&ircUserModeRestricted { -		*mode += "r" +		*mode = append(*mode, 'r')  	}  	if 0 != m&ircUserModeOperator { -		*mode += "o" +		*mode = append(*mode, 'o')  	}  	if 0 != m&ircUserModeRxServerNotices { -		*mode += "s" +		*mode = append(*mode, 's')  	}  }  func (c *client) getMode() string { -	mode := "" +	var mode []byte  	if c.awayMessage != "" { -		mode += "a" +		mode = append(mode, 'a')  	}  	clientModeToString(c.mode, &mode) -	return mode +	return string(mode)  }  func (c *client) send(line string) { @@ -571,14 +612,13 @@ func (c *client) send(line string) {  		return  	} -	// TODO: Rename inQ and outQ to recvQ and sendQ as they are usually named. -	oldOutQ := len(c.outQ) +	oldSendQLen := len(c.sendQ)  	// So far there's only one message tag we use, so we can do it simple;  	// note that a 1024-character limit applies to messages with tags on.  	if 0 != c.capsEnabled&ircCapServerTime { -		c.outQ = time.Now().UTC(). -			AppendFormat(c.outQ, "@time=2006-01-02T15:04:05.000Z ") +		c.sendQ = time.Now().UTC(). +			AppendFormat(c.sendQ, "@time=2006-01-02T15:04:05.000Z ")  	}  	bytes := []byte(line) @@ -587,13 +627,13 @@ func (c *client) send(line string) {  	}  	// TODO: Kill the connection above some "SendQ" threshold (careful!) -	c.outQ = append(c.outQ, bytes...) -	c.outQ = append(c.outQ, "\r\n"...) -	c.flushOutQ() +	c.sendQ = append(c.sendQ, bytes...) +	c.sendQ = append(c.sendQ, "\r\n"...) +	c.flushSendQ()  	// Technically we haven't sent it yet but that's a minor detail  	c.nSentMessages++ -	c.sentBytes += len(c.outQ) - oldOutQ +	c.sentBytes += len(c.sendQ) - oldSendQLen  }  func (c *client) sendf(format string, a ...interface{}) { @@ -604,7 +644,14 @@ func (c *client) addToWhowas() {  	// Only keeping one entry for each nickname.  	// TODO: Make sure this list doesn't get too long, for example by  	// putting them in a linked list ordered by time. -	whowas[ircStrxfrm(c.nickname)] = newWhowasInfo(c) +	whowas[ircToCanon(c.nickname)] = newWhowasInfo(c) +} + +func (c *client) nicknameOrStar() string { +	if c.nickname == "" { +		return "*" +	} +	return c.nickname  }  func (c *client) unregister(reason string) { @@ -622,14 +669,13 @@ func (c *client) unregister(reason string) {  	}  	c.addToWhowas() -	delete(users, ircStrxfrm(c.nickname)) +	delete(users, ircToCanon(c.nickname))  	c.nickname = ""  	c.registered = false  } -// TODO: Rename to kill.  // Close the connection and forget about the client. -func (c *client) destroy(reason string) { +func (c *client) kill(reason string) {  	if reason == "" {  		reason = "Client exited"  	} @@ -661,25 +707,20 @@ func (c *client) closeLink(reason string) {  	// We also want to avoid accidentally writing to the socket before  	// address resolution has finished.  	if c.conn == nil { -		c.destroy(reason) +		c.kill(reason)  		return  	}  	if c.closing {  		return  	} -	nickname := c.nickname -	if nickname == "" { -		nickname = "*" -	} -  	// We push an "ERROR" message to the write buffer and let the writer send  	// it, with some arbitrary timeout. The "closing" state makes sure  	// that a/ we ignore any successive messages, and b/ that the connection  	// is killed after the write buffer is transferred and emptied.  	// (Since we send this message, we don't need to call CloseWrite here.)  	c.sendf("ERROR :Closing link: %s[%s] (%s)", -		nickname, c.hostname /* TODO host IP? */, reason) +		c.nicknameOrStar(), c.hostname /* TODO host IP? */, reason)  	c.closing = true  	c.unregister(reason) @@ -720,12 +761,7 @@ func (c *client) getTLSCertFingerprint() string {  // XXX: ap doesn't really need to be a slice.  func (c *client) makeReply(id int, ap []interface{}) string { -	nickname := c.nickname -	if nickname == "" { -		nickname = "*" -	} - -	s := fmt.Sprintf(":%s %03d %s ", serverName, id, nickname) +	s := fmt.Sprintf(":%s %03d %s ", serverName, id, c.nicknameOrStar())  	a := fmt.Sprintf(defaultReplies[id], ap...)  	return s + a  } @@ -809,7 +845,7 @@ func isThisMe(target string) bool {  	if ircFnmatch(target, serverName) {  		return true  	} -	_, ok := users[ircStrxfrm(target)] +	_, ok := users[ircToCanon(target)]  	return ok  } @@ -821,21 +857,20 @@ func (c *client) sendISUPPORT() {  }  func (c *client) tryFinishRegistration() { -	// TODO: Check if the realname is really required. -	if c.nickname == "" || c.username == "" || c.realname == "" { +	if c.registered || c.capNegotiating {  		return  	} -	if c.registered || c.capNegotiating { +	if c.nickname == "" || c.username == "" {  		return  	}  	c.registered = true  	c.sendReply(RPL_WELCOME, c.nickname, c.username, c.hostname) -	c.sendReply(RPL_YOURHOST, serverName, "TODO version") +	c.sendReply(RPL_YOURHOST, serverName, projectVersion)  	// The purpose of this message eludes me.  	c.sendReply(RPL_CREATED, time.Unix(started, 0).Format("Mon, 02 Jan 2006")) -	c.sendReply(RPL_MYINFO, serverName, "TODO version", +	c.sendReply(RPL_MYINFO, serverName, projectVersion,  		ircSupportedUserModes, ircSupportedChanModes)  	c.sendISUPPORT() @@ -852,7 +887,7 @@ func (c *client) tryFinishRegistration() {  			serverName, c.nickname, c.tlsCertFingerprint)  	} -	delete(whowas, ircStrxfrm(c.nickname)) +	delete(whowas, ircToCanon(c.nickname))  }  // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -957,8 +992,6 @@ func (c *client) handleCAPEND(a *ircCapArgs) {  // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -// TODO: Beware of case sensitivity, probably need to index it by ircStrxfrm, -// which should arguably be named ircToLower and ircToUpper or something.  var ircCapHandlers = map[string]func(*client, *ircCapArgs){  	"LS":   (*client).handleCAPLS,  	"LIST": (*client).handleCAPLIST, @@ -976,14 +1009,8 @@ func ircHandleCAP(msg *message, c *client) {  		return  	} -	// TODO: This really does seem to warrant a method. -	nickname := c.nickname -	if nickname == "" { -		nickname = "*" -	} -  	args := &ircCapArgs{ -		target:     nickname, +		target:     c.nicknameOrStar(),  		subcommand: msg.params[0],  		fullParams: "",  		params:     []string{}, @@ -991,12 +1018,10 @@ func ircHandleCAP(msg *message, c *client) {  	if len(msg.params) > 1 {  		args.fullParams = msg.params[1] -		// TODO: ignore_empty, likely create SplitSkipEmpty -		args.params = strings.Split(args.fullParams, " ") +		args.params = splitString(args.fullParams, " ", true)  	} -	// FIXME: We should ASCII ToUpper the subcommand. -	if fn, ok := ircCapHandlers[ircStrxfrm(args.subcommand)]; !ok { +	if fn, ok := ircCapHandlers[ircToCanon(args.subcommand)]; !ok {  		c.sendReply(ERR_INVALIDCAPCMD, args.subcommand,  			"Invalid CAP subcommand")  	} else { @@ -1026,8 +1051,8 @@ func ircHandleNICK(msg *message, c *client) {  		return  	} -	nicknameNormalized := ircStrxfrm(nickname) -	if client, ok := users[nicknameNormalized]; ok && client != c { +	nicknameCanon := ircToCanon(nickname) +	if client, ok := users[nicknameCanon]; ok && client != c {  		c.sendReply(ERR_NICKNAMEINUSE, nickname)  		return  	} @@ -1043,11 +1068,11 @@ func ircHandleNICK(msg *message, c *client) {  	// Release the old nickname and allocate a new one.  	if c.nickname != "" { -		delete(users, ircStrxfrm(c.nickname)) +		delete(users, ircToCanon(c.nickname))  	}  	c.nickname = nickname -	users[nicknameNormalized] = c +	users[nicknameCanon] = c  	c.tryFinishRegistration()  } @@ -1064,7 +1089,7 @@ func ircHandleUSER(msg *message, c *client) {  	username, mode, realname := msg.params[0], msg.params[1], msg.params[3] -	// Unfortunately the protocol doesn't give us any means of rejecting it +	// Unfortunately, the protocol doesn't give us any means of rejecting it.  	if !ircIsValidUsername(username) {  		username = "*"  	} @@ -1091,7 +1116,30 @@ func ircHandleUSERHOST(msg *message, c *client) {  		return  	} -	// TODO +	var reply []byte +	for i := 0; i < 5 && i < len(msg.params); i++ { +		nick := msg.params[i] +		target := users[ircToCanon(nick)] +		if target == nil { +			continue +		} +		if i != 0 { +			reply = append(reply, ' ') +		} + +		reply = append(reply, nick...) +		if 0 != target.mode&ircUserModeOperator { +			reply = append(reply, '*') +		} + +		if target.awayMessage != "" { +			reply = append(reply, "=-"...) +		} else { +			reply = append(reply, "=+"...) +		} +		reply = append(reply, (target.username + "@" + target.hostname)...) +	} +	c.sendReply(RPL_USERHOST, string(reply))  }  func ircHandleLUSERS(msg *message, c *client) { @@ -1162,8 +1210,8 @@ func ircHandleVERSION(msg *message, c *client) {  		postVersion = 1  	} -	c.sendReply(RPL_VERSION, "TODO version", postVersion, serverName, -		"TODO program name"+" "+"TODO version") +	c.sendReply(RPL_VERSION, projectVersion, postVersion, serverName, +		projectName+" "+projectVersion)  	c.sendISUPPORT()  } @@ -1199,13 +1247,13 @@ func ircHandleMODE(msg *message, c *client) {  	// TODO  	target := msg.params[0] -	client := users[ircStrxfrm(target)] -	ch := users[ircStrxfrm(target)] +	client := users[ircToCanon(target)] +	ch := users[ircToCanon(target)]  	if client != nil { -		// TODO: Think about strcmp. -		//if ircStrcmp(target, c.nickname) != 0 { -		//} +		// TODO +		if ircEqual(target, c.nickname) { +		}  	} else if ch != nil {  		// TODO  	} @@ -1223,11 +1271,11 @@ func ircHandleUserMessage(msg *message, c *client,  	}  	target, text := msg.params[0], msg.params[1] -	if client, ok := users[ircStrxfrm(target)]; ok { +	if client, ok := users[ircToCanon(target)]; ok {  		// TODO  		_ = client  		_ = text -	} else if ch, ok := channels[ircStrxfrm(target)]; ok { +	} else if ch, ok := channels[ircToCanon(target)]; ok {  		// TODO  		_ = ch  	} else { @@ -1254,7 +1302,7 @@ func (c *client) onPrepared(host string, isTLS bool) {  	c.hostname = host  	c.address = net.JoinHostPort(host, c.port) -	// TODO: If we've tried to send any data before now, we need to flushOutQ. +	// TODO: If we've tried to send any data before now, we need to flushSendQ.  	go read(c)  	c.reading = true  } @@ -1266,16 +1314,16 @@ func (c *client) onRead(data []byte, readErr error) {  		return  	} -	c.inQ = append(c.inQ, data...) +	c.recvQ = append(c.recvQ, data...)  	for {  		// XXX: This accepts even simple LF newlines, even though they're not  		// really allowed by the protocol. -		advance, token, _ := bufio.ScanLines(c.inQ, false /* atEOF */) +		advance, token, _ := bufio.ScanLines(c.recvQ, false /* atEOF */)  		if advance == 0 {  			break  		} -		c.inQ = c.inQ[advance:] +		c.recvQ = c.recvQ[advance:]  		line := string(token)  		log.Printf("-> %s\n", line) @@ -1298,18 +1346,18 @@ func (c *client) onRead(data []byte, readErr error) {  		if readErr != io.EOF {  			log.Println(readErr) -			c.destroy("TODO") +			c.kill(readErr.Error())  		} else if c.closing {  			// Disregarding whether a clean shutdown has happened or not.  			log.Println("client finished shutdown") -			c.destroy("TODO") +			c.kill("TODO")  		} else {  			log.Println("client EOF")  			c.closeLink("")  		} -	} else if len(c.inQ) > 8192 { -		log.Println("client inQ overrun") -		c.closeLink("inQ overrun") +	} else if len(c.recvQ) > 8192 { +		log.Println("client recvQ overrun") +		c.closeLink("recvQ overrun")  		// tls.Conn doesn't have the CloseRead method (and it needs to be able  		// to read from the TCP connection even for writes, so there isn't much @@ -1319,29 +1367,29 @@ func (c *client) onRead(data []byte, readErr error) {  	}  } -// Spawn a goroutine to flush the outQ if possible and necessary. -func (c *client) flushOutQ() { +// Spawn a goroutine to flush the sendQ if possible and necessary. +func (c *client) flushSendQ() {  	if !c.writing && c.conn != nil { -		go write(c, c.outQ) +		go write(c, c.sendQ)  		c.writing = true  	}  }  // Handle the results from trying to write to the client connection.  func (c *client) onWrite(written int, writeErr error) { -	c.outQ = c.outQ[written:] +	c.sendQ = c.sendQ[written:]  	c.writing = false  	if writeErr != nil {  		log.Println(writeErr) -		c.destroy("TODO") -	} else if len(c.outQ) > 0 { -		c.flushOutQ() +		c.kill(writeErr.Error()) +	} else if len(c.sendQ) > 0 { +		c.flushSendQ()  	} else if c.closing {  		if c.reading {  			c.conn.CloseWrite()  		} else { -			c.destroy("TODO") +			c.kill("TODO")  		}  	}  } @@ -1419,7 +1467,7 @@ func read(client *client) {  	}  } -// Flush outQ, which is passed by parameter so that there are no data races. +// Flush sendQ, which is passed by parameter so that there are no data races.  func write(client *client, data []byte) {  	// We just write as much as we can, the main goroutine does the looping.  	n, err := client.conn.Write(data) @@ -1431,14 +1479,14 @@ func write(client *client, data []byte) {  func processOneEvent() {  	select {  	case <-sigs: -		if inShutdown { -			forceShutdown("requested by user") +		if quitting { +			forceQuit("requested by user")  		} else { -			initiateShutdown() +			initiateQuit()  		} -	case <-shutdownTimer: -		forceShutdown("timeout") +	case <-quitTimer: +		forceQuit("timeout")  	case conn := <-conns:  		log.Println("accepted client connection") @@ -1480,7 +1528,7 @@ func processOneEvent() {  	case c := <-timeouts:  		if _, ok := clients[c]; ok {  			log.Println("client timeouted") -			c.destroy("TODO") +			c.kill("TODO")  		}  	}  } @@ -1490,14 +1538,12 @@ func main() {  	version := flag.Bool("version", false, "show version and exit")  	flag.Parse() -	// TODO: Consider using the same version number for all subprojects.  	if *version { -		fmt.Printf("%s %s\n", "hid", "0") +		fmt.Printf("%s %s\n", projectName, projectVersion)  		return  	} -	// TODO: Configuration--create an INI parser, probably; -	//   lift XDG_CONFIG_HOME from gitlab-notifier. +	// TODO: Configuration--create an INI parser, probably.  	if len(flag.Args()) != 3 {  		log.Fatalf("usage: %s KEY CERT ADDRESS\n", os.Args[0])  	} @@ -1520,7 +1566,7 @@ func main() {  	go accept(listener)  	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) -	for !inShutdown || len(clients) > 0 { +	for !quitting || len(clients) > 0 {  		processOneEvent()  	}  } | 
