2026-10-08 16:09:04 +08:00
|
|
|
package core
|
2026-08-05 10:55:33 +08:00
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
2026-10-08 16:09:04 +08:00
|
|
|
"errors"
|
2026-08-05 10:55:33 +08:00
|
|
|
"fmt"
|
|
|
|
|
"runtime"
|
2026-09-05 20:09:28 +08:00
|
|
|
"sync/atomic"
|
2026-08-05 10:55:33 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type MethodCall struct {
|
|
|
|
|
ID string `json:"id,omitempty"`
|
|
|
|
|
Method CoreMethod `json:"method"`
|
|
|
|
|
Arguments json.RawMessage `json:"arguments"`
|
2026-09-25 03:56:58 +08:00
|
|
|
seq uint64
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-05 10:55:33 +08:00
|
|
|
type MethodError struct {
|
|
|
|
|
Code string `json:"code"`
|
|
|
|
|
Message string `json:"message"`
|
|
|
|
|
Details any `json:"details"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-08 16:09:04 +08:00
|
|
|
func (e *MethodError) Error() string {
|
|
|
|
|
return e.Message
|
2026-09-05 20:09:28 +08:00
|
|
|
}
|
|
|
|
|
|
2026-10-08 16:09:04 +08:00
|
|
|
type MethodResponse struct {
|
|
|
|
|
ID string `json:"id,omitempty"`
|
|
|
|
|
Result any `json:"result"`
|
|
|
|
|
Error *MethodError `json:"error,omitempty"`
|
2026-10-15 12:31:27 +08:00
|
|
|
File string `json:"file,omitempty"`
|
2026-08-05 10:55:33 +08:00
|
|
|
}
|
|
|
|
|
|
2026-10-08 16:09:04 +08:00
|
|
|
var methodCallSeq atomic.Uint64
|
2026-08-05 10:55:33 +08:00
|
|
|
|
2026-10-08 16:09:04 +08:00
|
|
|
// Each call runs on its own goroutine, so a handler that must honour the
|
|
|
|
|
// host's latest intent compares seq, stamped here in arrival order; every
|
|
|
|
|
// transport has to deliver calls in the order the host sent them.
|
|
|
|
|
func dispatchMethodCall(call *MethodCall, reply func(data []byte)) {
|
|
|
|
|
call.seq = methodCallSeq.Add(1)
|
|
|
|
|
// A developer-only fatal-path test. It runs outside every recovery so the
|
|
|
|
|
// core process terminates; on Android that is the whole application.
|
|
|
|
|
if call.Method == crashMethod {
|
|
|
|
|
go handleCrash()
|
|
|
|
|
return
|
2026-09-05 20:09:28 +08:00
|
|
|
}
|
2026-10-08 16:09:04 +08:00
|
|
|
safeGo(string(call.Method), func() {
|
|
|
|
|
reply(handleMethodCall(call))
|
|
|
|
|
})
|
2026-09-05 20:09:28 +08:00
|
|
|
}
|
|
|
|
|
|
2026-10-08 16:09:04 +08:00
|
|
|
func handleCrash() {
|
|
|
|
|
panic("handle invoke crash")
|
2026-08-05 10:55:33 +08:00
|
|
|
}
|
|
|
|
|
|
2026-10-08 16:09:04 +08:00
|
|
|
func handleMethodCall(call *MethodCall) (data []byte) {
|
|
|
|
|
defer func() {
|
|
|
|
|
if r := recover(); r != nil {
|
|
|
|
|
logError("panic in %s: %v\n%s", call.Method, r, stackTrace())
|
|
|
|
|
data = encodeResponse(MethodResponse{ID: call.ID, Error: &MethodError{
|
|
|
|
|
Code: "internal_error",
|
|
|
|
|
Message: fmt.Sprintf("internal panic: %v", r),
|
|
|
|
|
}})
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
handler, exists := methodHandlers[call.Method]
|
|
|
|
|
if !exists {
|
|
|
|
|
return encodeResponse(MethodResponse{ID: call.ID, Error: &MethodError{
|
|
|
|
|
Code: "not_implemented",
|
|
|
|
|
Message: fmt.Sprintf("unknown method: %s", call.Method),
|
|
|
|
|
}})
|
2026-09-05 20:09:28 +08:00
|
|
|
}
|
2026-10-08 16:09:04 +08:00
|
|
|
result, err := handler(call)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return encodeResponse(MethodResponse{ID: call.ID, Error: asMethodError(err)})
|
2026-08-05 10:55:33 +08:00
|
|
|
}
|
2026-10-15 12:31:27 +08:00
|
|
|
return spillResponse(call.ID, encodeResponse(MethodResponse{ID: call.ID, Result: result}))
|
2026-08-05 10:55:33 +08:00
|
|
|
}
|
|
|
|
|
|
2026-10-08 16:09:04 +08:00
|
|
|
func encodeResponse(response MethodResponse) []byte {
|
|
|
|
|
data, err := json.Marshal(response)
|
|
|
|
|
if err == nil {
|
|
|
|
|
return data
|
|
|
|
|
}
|
|
|
|
|
logError("MethodResponse marshal error: id=%s err=%v", response.ID, err)
|
|
|
|
|
data, _ = json.Marshal(MethodResponse{ID: response.ID, Error: &MethodError{
|
|
|
|
|
Code: "internal_error",
|
|
|
|
|
Message: "encode response: " + err.Error(),
|
|
|
|
|
}})
|
|
|
|
|
return data
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func asMethodError(err error) *MethodError {
|
|
|
|
|
var methodErr *MethodError
|
|
|
|
|
if errors.As(err, &methodErr) {
|
|
|
|
|
return methodErr
|
|
|
|
|
}
|
|
|
|
|
return &MethodError{Code: "core_error", Message: err.Error()}
|
2026-08-05 10:55:33 +08:00
|
|
|
}
|
|
|
|
|
|
2026-09-05 20:09:28 +08:00
|
|
|
func stackTrace() []byte {
|
|
|
|
|
buf := make([]byte, 4096)
|
|
|
|
|
return buf[:runtime.Stack(buf, false)]
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-08 16:09:04 +08:00
|
|
|
func safeGo(name string, run func()) {
|
2026-09-05 20:09:28 +08:00
|
|
|
go func() {
|
|
|
|
|
defer func() {
|
|
|
|
|
if r := recover(); r != nil {
|
2026-10-08 16:09:04 +08:00
|
|
|
logError("panic in %s: %v\n%s", name, r, stackTrace())
|
2026-09-05 20:09:28 +08:00
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
run()
|
2026-08-05 10:55:33 +08:00
|
|
|
}()
|
2026-09-05 20:09:28 +08:00
|
|
|
}
|
|
|
|
|
|
2026-10-08 16:09:04 +08:00
|
|
|
func (call MethodCall) decodeArguments(target any) error {
|
|
|
|
|
if len(call.Arguments) == 0 || string(call.Arguments) == "null" {
|
|
|
|
|
return errors.New("missing arguments")
|
|
|
|
|
}
|
|
|
|
|
return json.Unmarshal(call.Arguments, target)
|
2026-09-05 20:09:28 +08:00
|
|
|
}
|
|
|
|
|
|
2026-10-08 16:09:04 +08:00
|
|
|
func decodeArguments[P any](call *MethodCall) (params P, err error) {
|
|
|
|
|
if decodeErr := call.decodeArguments(¶ms); decodeErr != nil {
|
|
|
|
|
return params, &MethodError{
|
|
|
|
|
Code: "invalid_arguments",
|
|
|
|
|
Message: fmt.Sprintf("invalid arguments for %s: %v", call.Method, decodeErr),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return params, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type methodHandler func(call *MethodCall) (any, error)
|
|
|
|
|
|
|
|
|
|
func withArguments[P, R any](run func(P) R) methodHandler {
|
|
|
|
|
return withFallible(func(params P) (R, error) {
|
|
|
|
|
return run(params), nil
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-09-05 20:09:28 +08:00
|
|
|
|
2026-10-08 16:09:04 +08:00
|
|
|
func withFallible[P, R any](run func(P) (R, error)) methodHandler {
|
|
|
|
|
return func(call *MethodCall) (any, error) {
|
|
|
|
|
params, err := decodeArguments[P](call)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
2026-08-05 10:55:33 +08:00
|
|
|
}
|
2026-10-08 16:09:04 +08:00
|
|
|
return run(params)
|
2026-09-05 20:09:28 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-08 16:09:04 +08:00
|
|
|
// The host reads the result of these methods as a message that is empty on
|
|
|
|
|
// success. A *MethodError is the structured form and still fails the call.
|
|
|
|
|
func withMessage[P any](run func(P) error) methodHandler {
|
|
|
|
|
return withFallible(func(params P) (string, error) {
|
|
|
|
|
err := run(params)
|
|
|
|
|
var methodErr *MethodError
|
|
|
|
|
if err == nil || errors.As(err, &methodErr) {
|
|
|
|
|
return "", err
|
2026-09-05 20:09:28 +08:00
|
|
|
}
|
2026-10-08 16:09:04 +08:00
|
|
|
return err.Error(), nil
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func withoutArguments[R any](run func() R) methodHandler {
|
|
|
|
|
return func(*MethodCall) (any, error) {
|
|
|
|
|
return run(), nil
|
2026-09-05 20:09:28 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-08 16:09:04 +08:00
|
|
|
func acknowledged(run func()) methodHandler {
|
|
|
|
|
return func(*MethodCall) (any, error) {
|
|
|
|
|
run()
|
|
|
|
|
return true, nil
|
2026-09-05 20:09:28 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var methodHandlers = map[CoreMethod]methodHandler{
|
2026-10-08 16:09:04 +08:00
|
|
|
initClashMethod: withArguments(handleInitClash),
|
|
|
|
|
getIsInitMethod: withoutArguments(handleGetIsInit),
|
|
|
|
|
forceGcMethod: acknowledged(handleForceGC),
|
|
|
|
|
shutdownMethod: withoutArguments(handleShutdown),
|
|
|
|
|
startListenerMethod: withoutArguments(handleStartListener),
|
|
|
|
|
stopListenerMethod: withoutArguments(handleStopListener),
|
|
|
|
|
getMemoryStatsMethod: withoutArguments(handleGetMemoryStats),
|
|
|
|
|
startLogMethod: acknowledged(handleStartLog),
|
|
|
|
|
stopLogMethod: acknowledged(handleStopLog),
|
|
|
|
|
validateConfigMethod: withMessage(handleValidateConfig),
|
|
|
|
|
decryptConfigMethod: withFallible(handleDecryptConfig),
|
|
|
|
|
getConfigMethod: withFallible(handleGetConfig),
|
2026-10-15 12:31:27 +08:00
|
|
|
getProfileBaseMethod: withFallible(handleGetProfileBase),
|
|
|
|
|
getProfileOutboundsMethod: withFallible(handleGetProfileOutbounds),
|
|
|
|
|
buildProfileMethod: withFallible(handleBuildProfile),
|
2026-10-08 16:09:04 +08:00
|
|
|
setupConfigMethod: withMessage(handleSetupConfig),
|
|
|
|
|
updateConfigMethod: withMessage(updateConfig),
|
|
|
|
|
validateProxiesMethod: withArguments(handleValidateProxies),
|
|
|
|
|
validateFiltersMethod: withArguments(handleValidateFilters),
|
|
|
|
|
convertProxiesMethod: withFallible(handleConvertProxies),
|
|
|
|
|
encodeShareLinksMethod: withArguments(handleEncodeShareLinks),
|
|
|
|
|
decodeShareLinksMethod: withArguments(handleDecodeShareLinks),
|
|
|
|
|
getProxiesMethod: withoutArguments(handleGetProxies),
|
|
|
|
|
changeProxyMethod: withArguments(handleChangeProxy),
|
|
|
|
|
asyncTestDelayMethod: withArguments(handleTestDelay),
|
|
|
|
|
watchRouteMethod: watchRouteHandler,
|
|
|
|
|
probeMethod: withArguments(handleProbe),
|
|
|
|
|
outboundIpMethod: withArguments(handleOutboundIp),
|
|
|
|
|
serviceCheckMethod: withArguments(handleServiceCheck),
|
|
|
|
|
getTrafficMethod: withArguments(handleGetTraffic),
|
|
|
|
|
getTotalTrafficMethod: withArguments(handleGetTotalTraffic),
|
|
|
|
|
resetTrafficMethod: acknowledged(handleResetTraffic),
|
|
|
|
|
getConnectionsMethod: withoutArguments(handleGetConnections),
|
|
|
|
|
getConnectionCountMethod: withoutArguments(handleGetConnectionCount),
|
|
|
|
|
closeConnectionsMethod: withoutArguments(handleCloseConnections),
|
|
|
|
|
resetConnectionsMethod: withoutArguments(handleResetConnections),
|
|
|
|
|
closeConnectionMethod: withArguments(handleCloseConnection),
|
|
|
|
|
getExternalProvidersMethod: withoutArguments(handleGetExternalProviders),
|
|
|
|
|
getExternalProviderMethod: withArguments(handleGetExternalProvider),
|
|
|
|
|
updateExternalProviderMethod: withMessage(handleUpdateExternalProvider),
|
|
|
|
|
sideLoadExternalProviderMethod: withMessage(handleSideLoadExternalProvider),
|
|
|
|
|
updateGeoDataMethod: withMessage(handleUpdateGeoData),
|
|
|
|
|
dumpRuleSetMethod: withFallible(handleDumpRuleSet),
|
|
|
|
|
compileRuleSetMethod: withFallible(handleCompileRuleSet),
|
|
|
|
|
clearEffectMethod: withMessage(handleClearEffect),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func watchRouteHandler(call *MethodCall) (any, error) {
|
|
|
|
|
watch, err := decodeArguments[bool](call)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return handleWatchRoute(watch, call.seq), nil
|
2026-09-05 20:09:28 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func registerMethod(method CoreMethod, handler methodHandler) {
|
|
|
|
|
if _, exists := methodHandlers[method]; exists {
|
|
|
|
|
panic(fmt.Sprintf("duplicate handler for method %s", method))
|
|
|
|
|
}
|
|
|
|
|
methodHandlers[method] = handler
|
|
|
|
|
}
|