Skip to content

Commit a4dca5d

Browse files
authored
Merge pull request #602 from pionxe/feat/daemon-http-ux
feat(daemon): HTTP 唤醒页面美化、跨平台终端拉起、安装后自启动及错误指引增强
2 parents 2974bb9 + ee72c1a commit a4dca5d

13 files changed

Lines changed: 656 additions & 42 deletions

internal/cli/daemon_commands.go

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"io"
78
"net"
89
neturl "net/url"
910
"os"
@@ -25,6 +26,9 @@ var (
2526
installHTTPDaemon = urlscheme.InstallHTTPDaemon
2627
uninstallHTTPDaemon = urlscheme.UninstallHTTPDaemon
2728
getHTTPDaemonStatus = urlscheme.GetHTTPDaemonStatus
29+
30+
daemonInstallJSONWriter io.Writer = os.Stdout
31+
daemonInstallLogWriter io.Writer = os.Stderr
2832
)
2933

3034
type daemonServeCommandOptions struct {
@@ -287,13 +291,26 @@ func defaultDaemonInstallCommandRunner(ctx context.Context, options daemonInstal
287291
ListenAddress: options.ListenAddress,
288292
})
289293
if err != nil {
294+
_, _ = fmt.Fprintf(daemonInstallLogWriter, "daemon install failed: %v\n", err)
295+
_, _ = fmt.Fprintf(daemonInstallLogWriter, "remedy: run `%s daemon install --listen %s`\n", executablePath, options.ListenAddress)
290296
return err
291297
}
292-
return encodeJSONLine(os.Stdout, map[string]any{
293-
"status": "ok",
294-
"listen_address": result.ListenAddress,
295-
"autostart_mode": result.AutostartMode,
296-
"hosts_warning": strings.TrimSpace(result.HostsWarning),
298+
_, _ = fmt.Fprintf(daemonInstallLogWriter, "daemon install succeeded: autostart=%s listen=%s\n", result.AutostartMode, result.ListenAddress)
299+
if strings.TrimSpace(result.HostsWarning) != "" {
300+
_, _ = fmt.Fprintf(daemonInstallLogWriter, "hosts warning: %s\n", strings.TrimSpace(result.HostsWarning))
301+
}
302+
if result.DaemonStarted {
303+
_, _ = fmt.Fprintf(daemonInstallLogWriter, "daemon started in background and is ready on %s\n", result.ListenAddress)
304+
} else if strings.TrimSpace(result.DaemonStartWarning) != "" {
305+
_, _ = fmt.Fprintf(daemonInstallLogWriter, "daemon startup warning: %s\n", strings.TrimSpace(result.DaemonStartWarning))
306+
}
307+
return encodeJSONLine(daemonInstallJSONWriter, map[string]any{
308+
"status": "ok",
309+
"listen_address": result.ListenAddress,
310+
"autostart_mode": result.AutostartMode,
311+
"hosts_warning": strings.TrimSpace(result.HostsWarning),
312+
"daemon_started": result.DaemonStarted,
313+
"daemon_start_warning": strings.TrimSpace(result.DaemonStartWarning),
297314
})
298315
}
299316

internal/cli/daemon_commands_test.go

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package cli
22

33
import (
4+
"bytes"
45
"context"
6+
"encoding/json"
57
"errors"
68
"net/url"
7-
"os"
89
"strings"
910
"testing"
1011

@@ -42,25 +43,34 @@ func TestDaemonInstallDefaultRunnerUsesCurrentExecutable(t *testing.T) {
4243
originalRunner := runDaemonInstallCommand
4344
originalResolveExecutablePath := resolveExecutablePath
4445
originalInstall := installHTTPDaemon
46+
originalJSONWriter := daemonInstallJSONWriter
47+
originalLogWriter := daemonInstallLogWriter
4548
t.Cleanup(func() { runDaemonInstallCommand = originalRunner })
4649
t.Cleanup(func() { resolveExecutablePath = originalResolveExecutablePath })
4750
t.Cleanup(func() { installHTTPDaemon = originalInstall })
51+
t.Cleanup(func() { daemonInstallJSONWriter = originalJSONWriter })
52+
t.Cleanup(func() { daemonInstallLogWriter = originalLogWriter })
4853

4954
runDaemonInstallCommand = defaultDaemonInstallCommandRunner
5055
resolveExecutablePath = func() (string, error) {
5156
return "/tmp/neocode", nil
5257
}
58+
var stdout bytes.Buffer
59+
var stderr bytes.Buffer
60+
daemonInstallJSONWriter = &stdout
61+
daemonInstallLogWriter = &stderr
5362
var captured urlscheme.HTTPDaemonInstallOptions
5463
installHTTPDaemon = func(options urlscheme.HTTPDaemonInstallOptions) (urlscheme.HTTPDaemonInstallResult, error) {
5564
captured = options
5665
return urlscheme.HTTPDaemonInstallResult{
57-
ListenAddress: options.ListenAddress,
58-
AutostartMode: "test-mode",
66+
ListenAddress: options.ListenAddress,
67+
AutostartMode: "test-mode",
68+
DaemonStarted: true,
69+
DaemonStartWarning: "",
5970
}, nil
6071
}
6172

6273
command := NewRootCommand()
63-
command.SetOut(os.Stdout)
6474
command.SetArgs([]string{"daemon", "install", "--listen", "127.0.0.1:19921"})
6575
if err := command.ExecuteContext(context.Background()); err != nil {
6676
t.Fatalf("ExecuteContext() error = %v", err)
@@ -71,6 +81,57 @@ func TestDaemonInstallDefaultRunnerUsesCurrentExecutable(t *testing.T) {
7181
if captured.ListenAddress != "127.0.0.1:19921" {
7282
t.Fatalf("listen address = %q, want %q", captured.ListenAddress, "127.0.0.1:19921")
7383
}
84+
var payload map[string]any
85+
if err := json.Unmarshal(bytes.TrimSpace(stdout.Bytes()), &payload); err != nil {
86+
t.Fatalf("decode stdout json: %v", err)
87+
}
88+
if payload["status"] != "ok" {
89+
t.Fatalf("status = %v, want ok", payload["status"])
90+
}
91+
if payload["daemon_started"] != true {
92+
t.Fatalf("daemon_started = %v, want true", payload["daemon_started"])
93+
}
94+
if !strings.Contains(stderr.String(), "daemon install succeeded") {
95+
t.Fatalf("stderr = %q, want success summary", stderr.String())
96+
}
97+
}
98+
99+
func TestDaemonInstallDefaultRunnerFailureWritesRemedy(t *testing.T) {
100+
originalRunner := runDaemonInstallCommand
101+
originalResolveExecutablePath := resolveExecutablePath
102+
originalInstall := installHTTPDaemon
103+
originalJSONWriter := daemonInstallJSONWriter
104+
originalLogWriter := daemonInstallLogWriter
105+
t.Cleanup(func() { runDaemonInstallCommand = originalRunner })
106+
t.Cleanup(func() { resolveExecutablePath = originalResolveExecutablePath })
107+
t.Cleanup(func() { installHTTPDaemon = originalInstall })
108+
t.Cleanup(func() { daemonInstallJSONWriter = originalJSONWriter })
109+
t.Cleanup(func() { daemonInstallLogWriter = originalLogWriter })
110+
111+
runDaemonInstallCommand = defaultDaemonInstallCommandRunner
112+
resolveExecutablePath = func() (string, error) {
113+
return "/tmp/neocode", nil
114+
}
115+
var stdout bytes.Buffer
116+
var stderr bytes.Buffer
117+
daemonInstallJSONWriter = &stdout
118+
daemonInstallLogWriter = &stderr
119+
installHTTPDaemon = func(options urlscheme.HTTPDaemonInstallOptions) (urlscheme.HTTPDaemonInstallResult, error) {
120+
return urlscheme.HTTPDaemonInstallResult{}, errors.New("boom")
121+
}
122+
123+
command := NewRootCommand()
124+
command.SetArgs([]string{"daemon", "install", "--listen", "127.0.0.1:19921"})
125+
err := command.ExecuteContext(context.Background())
126+
if err == nil {
127+
t.Fatal("expected install failure")
128+
}
129+
if !strings.Contains(stderr.String(), "remedy: run `/tmp/neocode daemon install --listen 127.0.0.1:19921`") {
130+
t.Fatalf("stderr = %q, want remedy command", stderr.String())
131+
}
132+
if stdout.Len() != 0 {
133+
t.Fatalf("stdout should be empty on failure, got %q", stdout.String())
134+
}
74135
}
75136

76137
func TestDaemonServeDoesNotExposeTokenFileFlag(t *testing.T) {

0 commit comments

Comments
 (0)