/* Package door: a go implementation of a BBS door for linux and Windows that uses door32.sys, understand CP437 and unicode (if available), detects screen size, and supports TheDraw Fonts. import ( "door" ) int main() { d = door.Door{} d.Init() // Process commandline switches, initialize door, detect screen size. d.Write("Welcome to my awesome door, written in "+door.ColorText("BLINK BOLD WHITE")+"go"+door.Reset+"."+door.CRNL) d.Write("Press a key...") d.Key() d.Write(door.CRNL) } */ package door import ( "bufio" "flag" "fmt" "log" "os" "path/filepath" "runtime/debug" "strconv" "sync" "time" "golang.org/x/term" ) const SavePos = "\x1b[s" // Save Cursor Position const RestorePos = "\x1b[u" // Restore Cursor Position const CRNL = "\r\n" // BBS Line Ending const Clrscr = "\x1b[0m\x1b[2J\x1b[H" // Clear screen, home cursor const HideCursor = "\x1b[?25l" // Hide Cursor const ShowCursor = "\x1b[?25h" // Show Cursor var Reset string = Color(0) // ANSI Color Reset const CURSOR_POS = "\x1b[6n" const SAVE_POS = "\x1b[s" const RESTORE_POS = "\x1b[u" // Mouse Clicks (On Click) const MOUSE_X10 = "\x1b[?9h" const MOUSE_X10_OFF = "\x1b[?9l" // Mouse Drags (Up/Down events) const MOUSE_DRAG = "\x1b[?1000h" const MOUSE_DRAG_OFF = "\x1b[?1000l" // Move these into the door structure, instead of having globals. var Unicode bool // Unicode support detected var CP437 bool // CP437 support detected var Full_CP437 bool // Full CP437 support detected (handles control codes properly) var Height int // Screen height detected var Width int // Screen width detected var Inactivity time.Duration = time.Duration(120) * time.Second // Inactivity timeout type CursorPos struct { X, Y int } type Mouse struct { Button int8 X int8 Y int8 } type MouseMode int const ( Off MouseMode = 0 X10 = 9 Normal = 1000 Button = 1002 AnyEvent = 1003 ) /* door32.sys: 0 Line 1 : Comm type (0=local, 1=serial, 2=telnet) 0 Line 2 : Comm or socket handle 38400 Line 3 : Baud rate Mystic 1.07 Line 4 : BBSID (software name and version) 1 Line 5 : User record position (1-based) James Coyle Line 6 : User's real name g00r00 Line 7 : User's handle/alias 255 Line 8 : User's security level 58 Line 9 : User's time left (in minutes) 1 Line 10: Emulation *See Below 1 Line 11: Current node number */ // Door32 information type DropfileConfig struct { Comm_type int // Comm type (0 local, 2 telnet "linux fd") Comm_handle int // Handle to use to talk to the user Baudrate int // (not used) BBSID string // BBS Software name User_number int // User number Real_name string // User's Real Name Handle string // User's Handle/Nick Security_level int // Security Level (if given) Time_left int // Time Left (minutes) Emulation int // (not used) Node int // BBS Node number } type ReaderData struct { R rune Ex Extended Err error } type Door struct { Config DropfileConfig READFD int WRITEFD int Disconnected bool // int32 // atomic bool // Has User disconnected/Hung up? TimeOut time.Time // Fixed point in time, when time expires StartTime time.Time // Time when User started door Pushback FIFOBuffer[rune] // Key buffer LastColor []int // Track the last color sent for restore color ReaderClosed bool // Reader close readerChannel chan ReaderData // Reading from the User readerMutex sync.Mutex // Reader close mutex ReaderCanClose bool // We can close the reader (in tests) WriterClosed bool // Writer closed writerChannel chan string // Writing to the User writerMutex sync.RWMutex LastMouse []Mouse // Store Mouse information LastCursor []CursorPos // Store Cursor pos information mcMutex sync.Mutex // Lock for LastMouse, LastCursor wg sync.WaitGroup tio_default *term.State // Terminal State to restore Mouse MouseMode // Mouse mode enabled } func (d *Door) SafeWriterClose() { d.writerMutex.Lock() defer d.writerMutex.Unlock() if !d.WriterClosed { d.WriterClosed = true close(d.writerChannel) } } func (d *Door) WriterIsClosed() bool { d.writerMutex.RLock() defer d.writerMutex.RUnlock() return d.WriterClosed } func (d *Door) AddMouse(mouse Mouse) { d.mcMutex.Lock() defer d.mcMutex.Unlock() d.LastMouse = append(d.LastMouse, mouse) } func (d *Door) GetMouse() (Mouse, bool) { d.mcMutex.Lock() defer d.mcMutex.Unlock() return ArrayDelete(&d.LastMouse, 0) } /* Enable mouse support 9 : X10 Support 1000: Normal 1002: Button Event 1003: Any-Event */ func (d *Door) EnableMouse(mode MouseMode) { if d.Mouse != Off { // Disable current mode first d.DisableMouse() } d.Mouse = mode if d.Mouse != Off { d.Write(fmt.Sprintf("\x1b[?%dh", int(d.Mouse))) } } // Disable mouse support func (d *Door) DisableMouse() { if d.Mouse != Off { d.Write(fmt.Sprintf("\x1b[?%dl", int(d.Mouse))) } d.Mouse = Off } func (d *Door) GetCursorPos() (CursorPos, bool) { d.mcMutex.Lock() defer d.mcMutex.Unlock() return ArrayDelete(&d.LastCursor, 0) } func (d *Door) ClearMouseCursor() { d.mcMutex.Lock() defer d.mcMutex.Unlock() d.LastMouse = make([]Mouse, 0, 2) d.LastCursor = make([]CursorPos, 0, 3) } // Return the amount of time left as time.Duration func (d *Door) TimeLeft() time.Duration { return time.Until(d.TimeOut) } func (d *Door) TimeUsed() time.Duration { return time.Since(d.StartTime) } func (d *Door) Disconnect() bool { return d.Disconnected // atomic.LoadInt32(&d.Disconnected) != 0 } // Read the BBS door file. We only support door32.sys. func (d *Door) ReadDropfile(filename string) { var file *os.File var err error file, err = os.Open(filename) if err != nil { log.Panicf("Open(%s): %s\n", filename, err) } defer file.Close() var lines []string // read line by line // The scanner handles DOS and linux file endings. var scanner *bufio.Scanner = bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() lines = append(lines, line) } d.Config.Comm_type, err = strconv.Atoi(lines[0]) if err != nil { log.Panicf("Door32 Comm Type (expected integer): %s\n", err) } d.Config.Comm_handle, err = strconv.Atoi(lines[1]) if err != nil { log.Panicf("Door32 Comm Handle (expected integer): %s\n", err) } d.Config.Baudrate, err = strconv.Atoi(lines[2]) if err != nil { log.Panicf("Door32 Baudrate (expected integer): %s\n", err) } d.Config.BBSID = lines[3] d.Config.User_number, err = strconv.Atoi(lines[4]) if err != nil { log.Panicf("Door32 User Number (expected integer): %s\n", err) } d.Config.Real_name = lines[5] d.Config.Handle = lines[6] d.Config.Security_level, err = strconv.Atoi(lines[7]) if err != nil { log.Panicf("Door32 Security Level (expected integer): %s\n", err) } d.Config.Time_left, err = strconv.Atoi(lines[8]) if err != nil { log.Panicf("Door32 Time Left (expected integer): %s\n", err) } d.Config.Emulation, err = strconv.Atoi(lines[9]) if err != nil { log.Panicf("Door32 Emulation (expected integer): %s\n", err) } d.Config.Node, err = strconv.Atoi(lines[10]) if err != nil { log.Panicf("Door32 Node Number (expected integer): %s\n", err) } if d.Config.Comm_type == 0 { d.READFD = 0 d.WRITEFD = 1 // RAW MODE d.tio_default, _ = term.MakeRaw(d.READFD) } else if d.Config.Comm_type == 2 { d.READFD = d.Config.Comm_handle d.WRITEFD = d.Config.Comm_handle } else { log.Panicf("Unsupported Comm type %d\n", d.Config.Comm_type) } d.StartTime = time.Now() // Calculate when time expires. d.TimeOut = time.Now().Add(time.Duration(d.Config.Time_left) * time.Minute) } func (d *Door) Detect() bool { // detect is destructive ... make it non-destructive // destructive: clears/trashes the screen. var detect string = "\r\x03\x04" + CURSOR_POS + "\b \b\b \b" + "\r\u2615" + CURSOR_POS + "\b \b\b \b\b \b" + SAVE_POS + "\x1b[999C\x1b[999B" + CURSOR_POS + RESTORE_POS // hot beverage is 3 bytes long -- need 3 "\b \b" to erase. d.Write(detect) var info []CursorPos = make([]CursorPos, 0, 3) var done bool for !done { _, ex, err := d.WaitKey(time.Second) log.Println("WaitKey:", ex, err) if ex == CURSOR { cursor, ok := d.GetCursorPos() if ok { info = append(info, cursor) if len(info) == 3 { done = true } } } if err != nil { done = true } } if len(info) != 3 { // Detection FAILED. log.Println("Detect FAILED:", info) return false } // Ok! Let's see what we've got... var valid bool // Where did I get these numbers from? // linux term (telnet/ssh) [0].X = 1, [1].X = 3 // VS Code terminal: 1, 3 // https://docs.python.org/3/library/unicodedata.html // \u2615 is a fullwidth (2 char) unicode symbol! // SO, [1].X == 3 // and not 2. // syncterm [0].X = 3, [1].X = 4 // Magiterm [0].X = 3, [1].X = 4 // cp437 + telnet [0].X = 1, [1].X = 4 FullCP437 = False // ^ Fails FullCP437 test - characters codes ignored. // cp437plus + telnet 3, 4. (Has FullCP437 support.) // if (info[0].X == 1 || info[0].X == 3) && // (info[1].X == 2 || info[1].X == 3) { // Breakdown by detected type: // Unicode \x03 \x04 (control codes ignored) // Unicode \u2615 = fullwidth takes up 2 characters // CP437 \x03 \x04 Hearts Diamonds Symbols 2 characters // ^ Only works for FullCP437 // CP437 \u2615 = b'\xe2\x98\x95' 3 bytes // So info[1].X = 4 if info[0].X == 1 && info[1].X == 3 { Unicode = true valid = true } else { // info[1].X = 4 Unicode = false CP437 = true valid = true } if info[0].X == 3 { Full_CP437 = true } if !valid { log.Println("Detect FAILED (not valid):", info) return false } Width, Height = info[2].X, info[2].Y // Putty doesn't seem to restoring the cursor position (when detecting). d.Write(Goto(1, info[0].Y)) log.Printf("Unicode: %t, CP437: %t, Full %t, Screen %v\n", Unicode, CP437, Full_CP437, info[2]) return true } // Initialize door framework. Parse commandline, read dropfile, // detect terminal capabilities. func (d *Door) Init(doorname string) { var dropfile string d.Pushback = NewFIFOBuffer[rune](5) // Get path to binary, and chdir to it. var binaryPath string binaryPath, _ = os.Executable() binaryPath = filepath.Dir(binaryPath) _ = os.Chdir(binaryPath) flag.StringVar(&dropfile, "d", "", "Path to dropfile") flag.Parse() if len(dropfile) == 0 { flag.PrintDefaults() os.Exit(2) } d.ReadDropfile(dropfile) // Logfile will be doorname - node # var logfilename string = fmt.Sprintf("%s-%d.log", doorname, d.Config.Node) var logf *os.File var err error logf, err = os.OpenFile(logfilename, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666) if err != nil { log.Panicf("Error creating log file %s: %v", logfilename, err) } log.SetOutput(logf) log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) log.Printf("Loading dropfile %s\n", dropfile) log.Printf("BBS %s, User %s / Handle %s / File %d\n", d.Config.BBSID, d.Config.Real_name, d.Config.Handle, d.Config.Comm_handle) d.readerChannel = make(chan ReaderData, 16) // was 8 ? /* Ok, here's the issue. This blocks the go reader when this is full. It seems like it would be better to have a channel that receives rune, Extended instead. */ d.writerChannel = make(chan string) // unbuffered // changing this to unbound/sync hangs tests. // d.closeChannel = make(chan struct{}, 2) // reader & door.Close d.setupChannels() d.Detect() if Unicode { BOXES = BOXES_UNICODE BARS = BARS_UNICODE } else { BOXES = BOXES_CP437 BARS = BARS_CP437 } } func (d *Door) Close() { defer func() { if err := recover(); err != nil { log.Println("door.Close FAILURE:", err) // This displays stack trace stderr debug.PrintStack() } }() d.DisableMouse() log.Println("Closing...") // d.closeChannel <- struct{}{} close(d.writerChannel) /* if !d.WriterClosed { d.writerChannel <- "" } */ // CloseReader(d.Config.Comm_handle) log.Println("wg.Wait()") d.wg.Wait() log.Println("Closed.") if d.Config.Comm_type == 0 { term.Restore(d.READFD, d.tio_default) } } // Goto X, Y - Position the cursor using ANSI Escape Codes // // Example: // // d.Write(door.Goto(1, 5) + "Now at X=1, Y=5.") func Goto(x int, y int) string { return fmt.Sprintf("\x1b[%d;%dH", y, x) }