Skip to content

Commit f0ee0b0

Browse files
mapleafgoclaude
andcommitted
feat(ipc): 暴露服务安装卸载到 CLI 命令,VLESS flow 改为透传
新增 `singcast service install --home <dir>` / `singcast service uninstall` 命令, InstallService 接受 homeDir 参数而非内部获取工作目录。VLESS flow 字段不再写死 为 xtls-rprx-vision,改为直接透传用户配置值由 sing-box 运行时校验。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent ec57f31 commit f0ee0b0

7 files changed

Lines changed: 85 additions & 22 deletions

File tree

cmd/singcast/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ func main() {
2020
ipcCommand(),
2121
convertCommand(),
2222
checkCommand(),
23+
serviceCommand(),
2324
versionCommand(),
2425
},
2526
}

cmd/singcast/service.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/urfave/cli/v3"
8+
9+
"github.com/mapleafgo/singcast/ipc"
10+
)
11+
12+
func serviceCommand() *cli.Command {
13+
return &cli.Command{
14+
Name: "service",
15+
Usage: "Manage system service",
16+
Commands: []*cli.Command{
17+
{
18+
Name: "install",
19+
Usage: "Install the system service",
20+
Flags: []cli.Flag{
21+
&cli.StringFlag{
22+
Name: "home",
23+
Usage: "home directory for the service",
24+
Required: true,
25+
},
26+
},
27+
Action: func(ctx context.Context, cmd *cli.Command) error {
28+
if err := ipc.InstallService(cmd.String("home")); err != nil {
29+
return fmt.Errorf("install service: %w", err)
30+
}
31+
fmt.Println("service installed")
32+
return nil
33+
},
34+
},
35+
{
36+
Name: "uninstall",
37+
Usage: "Uninstall the system service",
38+
Action: func(ctx context.Context, cmd *cli.Command) error {
39+
if err := ipc.UninstallService(); err != nil {
40+
return fmt.Errorf("uninstall service: %w", err)
41+
}
42+
fmt.Println("service uninstalled")
43+
return nil
44+
},
45+
},
46+
},
47+
}
48+
}

ipc/handler.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ func (h *Handler) Handle(req *JSONRPCRequest) JSONRPCResponse {
7575
case MethodSetLogLevel:
7676
return h.handleSetLogLevel(req, id)
7777
case MethodServiceInstall:
78-
return h.handleVoid(req, id, func() error { return InstallService() })
78+
return h.handleServiceInstall(req, id)
7979
case MethodServiceUninstall:
8080
return h.handleVoid(req, id, func() error { return UninstallService() })
8181
default:
@@ -176,6 +176,17 @@ func (h *Handler) handleSetLogLevel(req *JSONRPCRequest, id int64) JSONRPCRespon
176176
return newEmptyResult(id)
177177
}
178178

179+
func (h *Handler) handleServiceInstall(req *JSONRPCRequest, id int64) JSONRPCResponse {
180+
var params InstallServiceParams
181+
if err := json.Unmarshal(req.Params, &params); err != nil {
182+
return newError(id, -32602, "invalid params: "+err.Error())
183+
}
184+
if err := InstallService(params.Home); err != nil {
185+
return newError(id, 1, err.Error())
186+
}
187+
return newEmptyResult(id)
188+
}
189+
179190
func (h *Handler) handleVoid(req *JSONRPCRequest, id int64, fn func() error) JSONRPCResponse {
180191
if err := fn(); err != nil {
181192
return newError(id, 1, err.Error())

ipc/service_posix.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ package ipc
44

55
import "errors"
66

7-
func InstallService() error {
7+
func InstallService(_ string) error {
88
return errors.New("service management is only supported on Windows")
99
}
1010

ipc/service_windows.go

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,12 @@ import (
1313

1414
const ServiceName = "SingcastService"
1515

16-
func InstallService() error {
16+
func InstallService(homeDir string) error {
1717
exe, err := os.Executable()
1818
if err != nil {
1919
return fmt.Errorf("get executable: %w", err)
2020
}
2121

22-
homeDir, err := os.Getwd()
23-
if err != nil {
24-
return fmt.Errorf("get working directory: %w", err)
25-
}
26-
2722
m, err := mgr.Connect()
2823
if err != nil {
2924
return fmt.Errorf("connect to SCM: %w", err)
@@ -65,13 +60,20 @@ func UninstallService() error {
6560
}
6661
defer m.Disconnect()
6762

68-
s, err := m.OpenService(ServiceName)
63+
namePtr, err := windows.UTF16PtrFromString(ServiceName)
6964
if err != nil {
70-
return fmt.Errorf("service %s not found: %w", ServiceName, err)
65+
return err
7166
}
72-
defer s.Close()
7367

74-
if err := s.Delete(); err != nil {
68+
// Use windows.OpenService with DELETE only — mgr.OpenService hardcodes
69+
// SERVICE_ALL_ACCESS which requires standard rights the DACL does not grant.
70+
h, err := windows.OpenService(m.Handle, namePtr, windows.DELETE)
71+
if err != nil {
72+
return fmt.Errorf("open service %s: %w", ServiceName, err)
73+
}
74+
defer windows.CloseServiceHandle(h)
75+
76+
if err := windows.DeleteService(h); err != nil {
7577
return fmt.Errorf("delete service: %w", err)
7678
}
7779
return nil
@@ -83,15 +85,11 @@ func UninstallService() error {
8385
//
8486
// Access mask:
8587
//
86-
// SERVICE_QUERY_STATUS = 0x0004
87-
// SERVICE_START = 0x0010
88-
// SERVICE_STOP = 0x0020
89-
// SERVICE_INTERROGATE = 0x0080
90-
// Total = 0x00B4
91-
//
92-
// SIDs: AU = Authenticated Users, BA = Built-in Administrators, SY = Local System.
88+
// AU (Authenticated Users): 0x100B4 — DELETE + query status + start + stop + interrogate
89+
// BA (Built-in Admins): 0xF01FF — SERVICE_ALL_ACCESS
90+
// SY (Local System): 0xF01FF — SERVICE_ALL_ACCESS
9391
func setServiceDACL(serviceName string) error {
94-
sddl := "D:(A;;0x00B4;;;AU)(A;;0x01FF;;;BA)(A;;0x01FF;;;SY)"
92+
sddl := "D:(A;;0x100B4;;;AU)(A;;0xF01FF;;;BA)(A;;0xF01FF;;;SY)"
9593

9694
sd, err := windows.SecurityDescriptorFromString(sddl)
9795
if err != nil {

ipc/types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,11 @@ type SetLogLevelParams struct {
150150
Level int32 `json:"level"`
151151
}
152152

153+
// InstallServiceParams holds parameters for service.install.
154+
type InstallServiceParams struct {
155+
Home string `json:"home"`
156+
}
157+
153158
// --- Notification payload types ---
154159

155160
// StateUpdatePayload represents a kernel state change.

translator/proxy/vless.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ func TranslateVLESS(m map[string]any, warn func(string)) map[string]any {
1717
return nil
1818
}
1919

20-
// sing-box only supports "xtls-rprx-vision"; normalize all variants.
20+
// Pass flow as-is; sing-box validates at runtime.
2121
// https://github.com/mapleafgo/clash-for-flutter/issues/66
2222
if flow := GetStr(m, "flow"); flow != "" {
23-
outbound["flow"] = "xtls-rprx-vision"
23+
outbound["flow"] = flow
2424
}
2525

2626
// Packet encoding

0 commit comments

Comments
 (0)