Files
chen08209 2fa2f7f816 feat(core): bind the Android Core with gomobile
Changelog: Share link import and mrs rule sets
2026-10-11 12:28:52 +08:00

214 lines
4.5 KiB
Go

//go:build !android
package core
import (
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"sync"
"sync/atomic"
"time"
)
type ipcConn interface {
io.ReadWriteCloser
SetWriteDeadline(t time.Time) error
}
var (
conn ipcConn
connMu sync.Mutex
writeMu sync.Mutex
)
const (
maxIPCFrameSize = 64 * 1024 * 1024
ipcWriteTimeout = 10 * time.Second
)
var deliveryFailureReported atomic.Bool
// logDeliveryError must not reach the mihomo logger: a log event is published
// to the log subscriber, batched, and handed back to send, so reporting a send
// failure through it feeds the failure straight back into itself.
func logDeliveryError(format string, args ...any) {
if deliveryFailureReported.Swap(true) {
return
}
fmt.Fprintf(os.Stderr, "[ERROR] "+format+"\n", args...)
}
func deliverEvent(data []byte) {
send(data)
}
// The desktop core exits with its host connection, so it is always heard.
func hasEventListener() bool {
return true
}
func writeFrame(w io.Writer, data []byte) (int, error) {
if len(data) > maxIPCFrameSize {
return 0, fmt.Errorf("IPC frame exceeds %d bytes", maxIPCFrameSize)
}
lenBuf := [4]byte{}
binary.LittleEndian.PutUint32(lenBuf[:], uint32(len(data)))
written, err := writeAll(w, lenBuf[:])
if err != nil {
return written, err
}
n, err := writeAll(w, data)
return written + n, err
}
type resumingWriter struct {
conn ipcConn
written int
}
func (writer *resumingWriter) Write(data []byte) (int, error) {
accepted := 0
for {
n, err := writer.conn.Write(data[accepted:])
accepted += n
writer.written += n
if err == nil {
return accepted, nil
}
if accepted >= len(data) || !writer.resume(err) {
return accepted, err
}
}
}
// A half-written frame is only in sync while this process keeps writing it,
// and a host the OS has suspended (Windows Modern Standby freezes the app but
// not a service's child) drains again whenever it wakes, so a stall is waited
// out for as long as it lasts. A dead host surfaces as a hard error instead.
func (writer *resumingWriter) resume(err error) bool {
if writer.written == 0 || !isWriteTimeout(err) {
return false
}
return writer.conn.SetWriteDeadline(time.Now().Add(ipcWriteTimeout)) == nil
}
// go-winio reports an expired deadline as its own ErrTimeout, which does not
// match os.ErrDeadlineExceeded; Timeout() is the contract every pipe shares.
func isWriteTimeout(err error) bool {
var timeout interface{ Timeout() bool }
return errors.As(err, &timeout) && timeout.Timeout()
}
func writeAll(w io.Writer, data []byte) (int, error) {
written := 0
for len(data) > 0 {
n, err := w.Write(data)
written += n
if err != nil {
return written, err
}
if n == 0 {
return written, io.ErrShortWrite
}
data = data[n:]
}
return written, nil
}
func readFrame(r io.Reader) ([]byte, error) {
lenBuf := [4]byte{}
if _, err := io.ReadFull(r, lenBuf[:]); err != nil {
return nil, err
}
length := binary.LittleEndian.Uint32(lenBuf[:])
if length > maxIPCFrameSize {
return nil, fmt.Errorf("IPC frame exceeds %d bytes", maxIPCFrameSize)
}
data := make([]byte, int(length))
if _, err := io.ReadFull(r, data); err != nil {
return nil, err
}
return data, nil
}
func send(data []byte) {
writeMu.Lock()
defer writeMu.Unlock()
connMu.Lock()
c := conn
connMu.Unlock()
if c == nil {
logDeliveryError("send conn nil")
return
}
if err := c.SetWriteDeadline(time.Now().Add(ipcWriteTimeout)); err != nil {
logDeliveryError("server write deadline error: %v", err)
}
written, err := writeFrame(&resumingWriter{conn: c}, data)
if err == nil {
deliveryFailureReported.Store(false)
return
}
if written == 0 {
logDeliveryError("server write error, dropped one frame: %v", err)
return
}
logDeliveryError("server write error after %d bytes: %v", written, err)
connMu.Lock()
if conn == c {
conn = nil
}
connMu.Unlock()
_ = c.Close()
}
func StartServer(arg string) {
dialed, err := dial(arg)
if err != nil {
panic(err.Error())
}
serve(dialed)
}
func serve(dialed ipcConn) {
defer releaseOnExit()
defer func() {
connMu.Lock()
c := conn
conn = nil
connMu.Unlock()
if c != nil {
_ = c.Close()
}
}()
connMu.Lock()
conn = dialed
deliveryFailureReported.Store(false)
connMu.Unlock()
for {
data, err := readFrame(dialed)
if err != nil {
if err != io.EOF {
logError("server read error: %v", err)
}
return
}
call := &MethodCall{}
if err := json.Unmarshal(data, call); err != nil {
logError("server unmarshal error: %v (data: %q)", err, data)
continue
}
dispatchMethodCall(call, send)
}
}