Refactor platform command api

This commit is contained in:
世界 2023-03-01 10:37:47 +08:00
parent 0ca344df5f
commit 5af8d001ae
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
11 changed files with 342 additions and 204 deletions

View file

@ -84,11 +84,12 @@ func buildiOS() {
"-libname=box",
}
if !debugEnabled {
args = append(args,
"-trimpath", "-ldflags=-s -w -buildid=", "-tags", "with_gvisor,with_clash_api",
args = append(
args, "-trimpath", "-ldflags=-s -w -buildid=",
"-tags", "with_gvisor,with_quic,with_wireguard,with_utls,with_clash_api",
)
} else {
args = append(args, "-tags", "with_gvisor,with_clash_api,debug")
args = append(args, "-tags", "with_gvisor,with_quic,with_wireguard,with_utls,with_clash_api,debug")
}
args = append(args, "./experimental/libbox")

View file

@ -0,0 +1,8 @@
//go:build darwin
package libbox
const (
CommandLog int32 = iota
CommandStatus
)

View file

@ -0,0 +1,69 @@
//go:build darwin
package libbox
import (
"encoding/binary"
"net"
"path/filepath"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
type CommandClient struct {
sockPath string
handler CommandClientHandler
conn net.Conn
options CommandClientOptions
}
type CommandClientOptions struct {
Command int32
StatusInterval int64
}
type CommandClientHandler interface {
Connected()
Disconnected(message string)
WriteLog(message string)
WriteStatus(message *StatusMessage)
}
func NewCommandClient(sharedDirectory string, handler CommandClientHandler, options *CommandClientOptions) *CommandClient {
return &CommandClient{
sockPath: filepath.Join(sharedDirectory, "command.sock"),
handler: handler,
options: common.PtrValueOrDefault(options),
}
}
func (c *CommandClient) Connect() error {
conn, err := net.DialUnix("unix", nil, &net.UnixAddr{
Name: c.sockPath,
Net: "unix",
})
if err != nil {
return err
}
c.conn = conn
err = binary.Write(conn, binary.BigEndian, uint8(c.options.Command))
if err != nil {
return err
}
switch c.options.Command {
case CommandLog:
go c.handleLogConn(conn)
case CommandStatus:
err = binary.Write(conn, binary.BigEndian, c.options.StatusInterval)
if err != nil {
return E.Cause(err, "write interval")
}
go c.handleStatusConn(conn)
}
return nil
}
func (c *CommandClient) Disconnect() error {
return common.Close(c.conn)
}

View file

@ -0,0 +1,104 @@
//go:build darwin
package libbox
import (
"context"
"encoding/binary"
"io"
"net"
)
func (s *CommandServer) WriteMessage(message string) {
s.subscriber.Emit(message)
s.access.Lock()
s.savedLines.PushBack(message)
if s.savedLines.Len() > 100 {
s.savedLines.Remove(s.savedLines.Front())
}
s.access.Unlock()
}
func readLog(reader io.Reader) ([]byte, error) {
var messageLength uint16
err := binary.Read(reader, binary.BigEndian, &messageLength)
if err != nil {
return nil, err
}
data := make([]byte, messageLength)
_, err = io.ReadFull(reader, data)
if err != nil {
return nil, err
}
return data, nil
}
func writeLog(writer io.Writer, message []byte) error {
err := binary.Write(writer, binary.BigEndian, uint16(len(message)))
if err != nil {
return err
}
_, err = writer.Write(message)
return err
}
func (s *CommandServer) handleLogConn(conn net.Conn) error {
var savedLines []string
s.access.Lock()
savedLines = make([]string, 0, s.savedLines.Len())
for element := s.savedLines.Front(); element != nil; element = element.Next() {
savedLines = append(savedLines, element.Value)
}
s.access.Unlock()
subscription, done, err := s.observer.Subscribe()
if err != nil {
return err
}
defer s.observer.UnSubscribe(subscription)
for _, line := range savedLines {
err = writeLog(conn, []byte(line))
if err != nil {
return err
}
}
ctx := connKeepAlive(conn)
for {
select {
case <-ctx.Done():
return ctx.Err()
case message := <-subscription:
err = writeLog(conn, []byte(message))
if err != nil {
return err
}
case <-done:
return nil
}
}
}
func (c *CommandClient) handleLogConn(conn net.Conn) {
c.handler.Connected()
for {
message, err := readLog(conn)
if err != nil {
c.handler.Disconnected(err.Error())
return
}
c.handler.WriteLog(string(message))
}
}
func connKeepAlive(reader io.Reader) context.Context {
ctx, cancel := context.WithCancelCause(context.Background())
go func() {
for {
_, err := readLog(reader)
if err != nil {
cancel(err)
return
}
}
}()
return ctx
}

View file

@ -0,0 +1,85 @@
//go:build darwin
package libbox
import (
"encoding/binary"
"net"
"os"
"path/filepath"
"sync"
"github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/observable"
"github.com/sagernet/sing/common/x/list"
)
type CommandServer struct {
sockPath string
listener net.Listener
access sync.Mutex
savedLines *list.List[string]
subscriber *observable.Subscriber[string]
observer *observable.Observer[string]
}
func NewCommandServer(sharedDirectory string) *CommandServer {
server := &CommandServer{
sockPath: filepath.Join(sharedDirectory, "command.sock"),
savedLines: new(list.List[string]),
subscriber: observable.NewSubscriber[string](128),
}
server.observer = observable.NewObserver[string](server.subscriber, 64)
return server
}
func (s *CommandServer) Start() error {
os.Remove(s.sockPath)
listener, err := net.ListenUnix("unix", &net.UnixAddr{
Name: s.sockPath,
Net: "unix",
})
if err != nil {
return err
}
go s.loopConnection(listener)
return nil
}
func (s *CommandServer) Close() error {
return s.listener.Close()
}
func (s *CommandServer) loopConnection(listener net.Listener) {
for {
conn, err := listener.Accept()
if err != nil {
return
}
go func() {
hErr := s.handleConnection(conn)
if hErr != nil && !E.IsClosed(err) {
log.Warn("log-server: process connection: ", hErr)
}
}()
}
}
func (s *CommandServer) handleConnection(conn net.Conn) error {
defer conn.Close()
var command uint8
err := binary.Read(conn, binary.BigEndian, &command)
if err != nil {
return E.Cause(err, "read command")
}
switch int32(command) {
case CommandLog:
return s.handleLogConn(conn)
case CommandStatus:
return s.handleStatusConn(conn)
default:
return E.New("unknown command: ", command)
}
}

View file

@ -0,0 +1,61 @@
//go:build darwin
package libbox
import (
"encoding/binary"
"net"
"runtime"
"time"
E "github.com/sagernet/sing/common/exceptions"
)
type StatusMessage struct {
Memory int64
Goroutines int32
}
func readStatus() StatusMessage {
var memStats runtime.MemStats
runtime.ReadMemStats(&memStats)
var message StatusMessage
message.Memory = int64(memStats.HeapInuse + memStats.StackInuse + memStats.MSpanInuse)
message.Goroutines = int32(runtime.NumGoroutine())
return message
}
func (s *CommandServer) handleStatusConn(conn net.Conn) error {
var interval int64
err := binary.Read(conn, binary.BigEndian, &interval)
if err != nil {
return E.Cause(err, "read interval")
}
ticker := time.NewTicker(time.Duration(interval))
defer ticker.Stop()
ctx := connKeepAlive(conn)
for {
err = binary.Write(conn, binary.BigEndian, readStatus())
if err != nil {
return err
}
select {
case <-ctx.Done():
return nil
case <-ticker.C:
}
}
}
func (c *CommandClient) handleStatusConn(conn net.Conn) {
c.handler.Connected()
for {
var message StatusMessage
err := binary.Read(conn, binary.BigEndian, &message)
if err != nil {
c.handler.Disconnected(err.Error())
return
}
c.handler.WriteStatus(&message)
}
}

View file

@ -1,59 +0,0 @@
//go:build darwin
package libbox
import (
"net"
"path/filepath"
"github.com/sagernet/sing/common"
)
type LogClient struct {
sockPath string
handler LogClientHandler
conn net.Conn
}
type LogClientHandler interface {
Connected()
Disconnected()
WriteLog(message string)
}
func NewLogClient(sharedDirectory string, handler LogClientHandler) *LogClient {
return &LogClient{
sockPath: filepath.Join(sharedDirectory, "log.sock"),
handler: handler,
}
}
func (c *LogClient) Connect() error {
conn, err := net.DialUnix("unix", nil, &net.UnixAddr{
Name: c.sockPath,
Net: "unix",
})
if err != nil {
return err
}
c.conn = conn
go c.loopConnection(&messageConn{conn})
return nil
}
func (c *LogClient) Disconnect() error {
return common.Close(c.conn)
}
func (c *LogClient) loopConnection(conn *messageConn) {
c.handler.Connected()
defer c.handler.Disconnected()
for {
message, err := conn.Read()
if err != nil {
c.handler.WriteLog("(log client error) " + err.Error())
return
}
c.handler.WriteLog(string(message))
}
}

View file

@ -1,139 +0,0 @@
//go:build darwin
package libbox
import (
"encoding/binary"
"io"
"net"
"os"
"path/filepath"
"sync"
"github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/observable"
"github.com/sagernet/sing/common/x/list"
)
type LogServer struct {
sockPath string
listener net.Listener
access sync.Mutex
savedLines *list.List[string]
subscriber *observable.Subscriber[string]
observer *observable.Observer[string]
}
func NewLogServer(sharedDirectory string) *LogServer {
server := &LogServer{
sockPath: filepath.Join(sharedDirectory, "log.sock"),
savedLines: new(list.List[string]),
subscriber: observable.NewSubscriber[string](128),
}
server.observer = observable.NewObserver[string](server.subscriber, 64)
return server
}
func (s *LogServer) Start() error {
os.Remove(s.sockPath)
listener, err := net.ListenUnix("unix", &net.UnixAddr{
Name: s.sockPath,
Net: "unix",
})
if err != nil {
return err
}
go s.loopConnection(listener)
return nil
}
func (s *LogServer) Close() error {
return s.listener.Close()
}
func (s *LogServer) WriteMessage(message string) {
s.subscriber.Emit(message)
s.access.Lock()
s.savedLines.PushBack(message)
if s.savedLines.Len() > 100 {
s.savedLines.Remove(s.savedLines.Front())
}
s.access.Unlock()
}
func (s *LogServer) loopConnection(listener net.Listener) {
for {
conn, err := listener.Accept()
if err != nil {
return
}
go func() {
hErr := s.handleConnection(&messageConn{conn})
if hErr != nil && !E.IsClosed(err) {
log.Warn("log-server: process connection: ", hErr)
}
}()
}
}
func (s *LogServer) handleConnection(conn *messageConn) error {
var savedLines []string
s.access.Lock()
savedLines = make([]string, 0, s.savedLines.Len())
for element := s.savedLines.Front(); element != nil; element = element.Next() {
savedLines = append(savedLines, element.Value)
}
s.access.Unlock()
subscription, done, err := s.observer.Subscribe()
if err != nil {
return err
}
defer s.observer.UnSubscribe(subscription)
for _, line := range savedLines {
err = conn.Write([]byte(line))
if err != nil {
return err
}
}
for {
select {
case message := <-subscription:
err = conn.Write([]byte(message))
if err != nil {
return err
}
case <-done:
conn.Close()
return nil
}
}
}
type messageConn struct {
net.Conn
}
func (c *messageConn) Read() ([]byte, error) {
var messageLength uint16
err := binary.Read(c.Conn, binary.BigEndian, &messageLength)
if err != nil {
return nil, err
}
data := make([]byte, messageLength)
_, err = io.ReadFull(c.Conn, data)
if err != nil {
return nil, err
}
return data, nil
}
func (c *messageConn) Write(message []byte) error {
err := binary.Write(c.Conn, binary.BigEndian, uint16(len(message)))
if err != nil {
return err
}
_, err = c.Conn.Write(message)
return err
}

View file

@ -1,6 +1,10 @@
package libbox
import C "github.com/sagernet/sing-box/constant"
import (
C "github.com/sagernet/sing-box/constant"
"github.com/dustin/go-humanize"
)
func SetBasePath(path string) {
C.SetBasePath(path)
@ -9,3 +13,7 @@ func SetBasePath(path string) {
func Version() string {
return C.Version
}
func FormatBytes(length int64) string {
return humanize.Bytes(uint64(length))
}

View file

@ -37,7 +37,7 @@ func startInstance(t *testing.T, options option.Options) *box.Box {
var instance *box.Box
var err error
for retry := 0; retry < 3; retry++ {
instance, err = box.New(ctx, options)
instance, err = box.New(ctx, options, nil)
require.NoError(t, err)
err = instance.Start()
if err != nil {

View file

@ -167,7 +167,7 @@ func testVLESSXray(t *testing.T, packetEncoding string, flow string) {
},
UUID: userID.String(),
Flow: flow,
PacketEncoding: packetEncoding,
PacketEncoding: &packetEncoding,
TLS: &option.OutboundTLSOptions{
Enabled: true,
ServerName: "example.org",