Cthulhu is no longer a fork. Now it is a screen reader written in Golang.
This commit is contained in:
1 parent
a998fe67de
commit
069fc674dc
708 files changed
+112482
-532
No files matched your search
@@ -0,0 +1,196 @@
|
||||
// Package nvda2cthulhu accepts the local WebSocket protocol used by NVDA
|
||||
// compatibility bridges and routes it to Cthulhu's speech and braille queues.
|
||||
package nvda2cthulhu
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPort = 3457
|
||||
hostEnv = "NVDA2SPEECHD_HOST"
|
||||
maxPayload = 64 * 1024
|
||||
)
|
||||
|
||||
// Handler accepts already-decoded bridge requests. Implementations must
|
||||
// enqueue backend work instead of waiting for speech or braille output.
|
||||
type Handler interface {
|
||||
SpeakText(string, bool) bool
|
||||
BrailleMessage(string) bool
|
||||
CancelSpeech() bool
|
||||
}
|
||||
|
||||
// Server owns the local bridge listener and active WebSocket connections.
|
||||
type Server struct {
|
||||
logger *slog.Logger
|
||||
handler Handler
|
||||
connections map[*websocket.Conn]struct{}
|
||||
connectionWait sync.WaitGroup
|
||||
stopOnce sync.Once
|
||||
stopped chan struct{}
|
||||
|
||||
mutex sync.Mutex
|
||||
server *http.Server
|
||||
}
|
||||
|
||||
// New creates a bridge server. A nil logger discards diagnostics.
|
||||
func New(logger *slog.Logger, handler Handler) *Server {
|
||||
if logger == nil {
|
||||
logger = slog.New(slog.DiscardHandler)
|
||||
}
|
||||
return &Server{
|
||||
logger: logger,
|
||||
handler: handler,
|
||||
connections: make(map[*websocket.Conn]struct{}),
|
||||
stopped: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Start binds the same local address used by Python Cthulhu. The listener is
|
||||
// established before Start returns, while connection handling stays async.
|
||||
func (server *Server) Start(ctx context.Context) error {
|
||||
listener, err := net.Listen("tcp", bindAddress())
|
||||
if err != nil {
|
||||
return fmt.Errorf("bind NVDA to Cthulhu bridge: %w", err)
|
||||
}
|
||||
httpServer := &http.Server{
|
||||
Handler: http.HandlerFunc(server.handleHTTP),
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
}
|
||||
server.mutex.Lock()
|
||||
server.server = httpServer
|
||||
server.mutex.Unlock()
|
||||
go func() {
|
||||
if err := httpServer.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
server.logger.Warn("NVDA to Cthulhu server stopped", "error", err)
|
||||
}
|
||||
}()
|
||||
server.logger.Info("NVDA to Cthulhu listening", "address", bindAddress())
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop closes the listener and active connections.
|
||||
func (server *Server) Stop(ctx context.Context) error {
|
||||
server.stopOnce.Do(func() {
|
||||
close(server.stopped)
|
||||
})
|
||||
server.mutex.Lock()
|
||||
httpServer := server.server
|
||||
connections := make([]*websocket.Conn, 0, len(server.connections))
|
||||
for connection := range server.connections {
|
||||
connections = append(connections, connection)
|
||||
}
|
||||
server.mutex.Unlock()
|
||||
for _, connection := range connections {
|
||||
_ = connection.Close()
|
||||
}
|
||||
if httpServer == nil {
|
||||
return server.waitConnections(ctx)
|
||||
}
|
||||
if err := httpServer.Shutdown(ctx); err != nil && !errors.Is(err, context.Canceled) {
|
||||
return fmt.Errorf("stop NVDA to Cthulhu bridge: %w", err)
|
||||
}
|
||||
return server.waitConnections(ctx)
|
||||
}
|
||||
|
||||
func (server *Server) handleHTTP(response http.ResponseWriter, request *http.Request) {
|
||||
if request.URL.Path != "/" {
|
||||
http.NotFound(response, request)
|
||||
return
|
||||
}
|
||||
upgrader := websocket.Upgrader{
|
||||
CheckOrigin: allowedOrigin,
|
||||
}
|
||||
connection, err := upgrader.Upgrade(response, request, nil)
|
||||
if err != nil {
|
||||
server.logger.Warn("NVDA to Cthulhu connection rejected", "error", err)
|
||||
return
|
||||
}
|
||||
defer connection.Close()
|
||||
server.mutex.Lock()
|
||||
select {
|
||||
case <-server.stopped:
|
||||
server.mutex.Unlock()
|
||||
return
|
||||
default:
|
||||
server.connectionWait.Add(1)
|
||||
server.connections[connection] = struct{}{}
|
||||
server.mutex.Unlock()
|
||||
}
|
||||
defer func() {
|
||||
server.mutex.Lock()
|
||||
delete(server.connections, connection)
|
||||
server.mutex.Unlock()
|
||||
server.connectionWait.Done()
|
||||
}()
|
||||
connection.SetReadLimit(maxPayload)
|
||||
server.logger.Debug("NVDA to Cthulhu client connected", "remote", connection.RemoteAddr().String())
|
||||
for {
|
||||
messageType, payload, err := connection.ReadMessage()
|
||||
if err != nil {
|
||||
if !websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
|
||||
server.logger.Debug("NVDA to Cthulhu client disconnected", "error", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if len(payload) > maxPayload {
|
||||
server.logger.Warn("NVDA to Cthulhu message too large", "size", len(payload))
|
||||
return
|
||||
}
|
||||
request, err := decodeRequest(messageType, payload)
|
||||
if err != nil {
|
||||
server.logger.Warn("NVDA to Cthulhu rejected message", "error", err)
|
||||
continue
|
||||
}
|
||||
server.deliver(request)
|
||||
}
|
||||
}
|
||||
|
||||
func (server *Server) waitConnections(ctx context.Context) error {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
server.connectionWait.Wait()
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func bindAddress() string {
|
||||
port := defaultPort
|
||||
if value := os.Getenv(hostEnv); value != "" {
|
||||
if parsed, err := url.Parse(value); err == nil && parsed.Port() != "" {
|
||||
port, _ = strconv.Atoi(parsed.Port())
|
||||
}
|
||||
}
|
||||
return net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
|
||||
}
|
||||
|
||||
func allowedOrigin(request *http.Request) bool {
|
||||
origin := request.Header.Get("Origin")
|
||||
if origin == "" {
|
||||
return true // Native bridge clients do not send a browser origin.
|
||||
}
|
||||
parsed, err := url.Parse(origin)
|
||||
if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.User != nil {
|
||||
return false
|
||||
}
|
||||
host := parsed.Hostname()
|
||||
return host == "localhost" || net.ParseIP(host).IsLoopback()
|
||||
}
|
||||
Reference in new issue
Block a user