Skip to content

Commit 697f856

Browse files
feat(cli): add restart command
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 3eca6e6 commit 697f856

8 files changed

Lines changed: 169 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ All apps auto-configure to use csghub-lite's OpenAI-compatible API endpoint with
157157
| `csghub-lite ps` | List currently running models and their keep-alive |
158158
| `csghub-lite stop <model>` | Stop/unload a running model |
159159
| `csghub-lite serve` | Start the API server (auto-started by `run`) |
160+
| `csghub-lite restart` | Restart the background API server |
160161
| `csghub-lite pull <model>` | Download a model from CSGHub |
161162
| `csghub-lite list` / `ls` | List locally downloaded models |
162163
| `csghub-lite show <model>` | Show model details (format, size, files) |

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ csghub-lite 是一个轻量级的本地大语言模型运行工具,基于 [CSG
1515
- [run](cli/run.md) — 自动下载并交互对话
1616
- [chat](cli/chat.md) — 与本地模型交互对话
1717
- [serve](cli/serve.md) — 启动 REST API 服务
18+
- [restart](cli/restart.md) — 重启后台 API 服务
1819
- [pull](cli/pull.md) — 下载模型
1920
- [list](cli/list.md) — 列出本地模型
2021
- [show](cli/show.md) — 查看模型详情

docs/cli/overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ csghub-lite 提供以下命令:
2626
|------|------|
2727
| [`ps`](ps.md) | 列出服务器上运行中的模型 |
2828
| [`stop <model>`](stop.md) | 停止/卸载服务器上运行中的模型 |
29+
| [`restart`](restart.md) | 重启后台 API 服务 |
2930

3031
## 配置与认证
3132

docs/cli/restart.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# csghub-lite restart
2+
3+
重启后台运行的 csghub-lite API 服务。如果当前没有可用的后台服务,也会直接启动一个新的后台服务。
4+
5+
## 用法
6+
7+
```bash
8+
csghub-lite restart
9+
```
10+
11+
## 说明
12+
13+
该命令会先尝试优雅停止现有服务,然后重新启动并等待健康检查通过。它使用当前配置中的 `listen_addr`,默认地址为 `:11435`
14+
15+
可用别名:
16+
17+
- `restart-service`
18+
- `restart-server`
19+
- `reload`
20+
21+
## 示例
22+
23+
```bash
24+
# 重启默认后台服务
25+
csghub-lite restart
26+
```

internal/cli/cli_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func TestNewRootCmd(t *testing.T) {
1919

2020
expected := []string{
2121
"serve", "apps", "run MODEL", "chat MODEL", "pull NAME", "list",
22-
"show MODEL", "ps", "stop MODEL", "stop-service",
22+
"show MODEL", "ps", "stop MODEL", "stop-service", "restart",
2323
"rm NAME", "login", "search QUERY", "config", "uninstall",
2424
}
2525
for _, name := range expected {

internal/cli/restart.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
"github.com/opencsgs/csghub-lite/internal/config"
8+
"github.com/spf13/cobra"
9+
)
10+
11+
var (
12+
stopBackgroundServiceForRestart = stopBackgroundServiceIfRunning
13+
startBackgroundServerForRestart = startBackgroundServer
14+
waitForServerForRestart = waitForServer
15+
)
16+
17+
func newRestartCmd() *cobra.Command {
18+
cmd := &cobra.Command{
19+
Use: "restart",
20+
Aliases: []string{"restart-service", "restart-server", "reload"},
21+
Short: "Restart the background csghub-lite service",
22+
Long: "Restart the background csghub-lite API service, starting it if it is not already running.",
23+
Args: cobra.NoArgs,
24+
RunE: runRestart,
25+
}
26+
return cmd
27+
}
28+
29+
func runRestart(cmd *cobra.Command, args []string) error {
30+
cfg, err := config.Load()
31+
if err != nil {
32+
return fmt.Errorf("loading config: %w", err)
33+
}
34+
return restartBackgroundService(cfg)
35+
}
36+
37+
func restartBackgroundService(cfg *config.Config) error {
38+
if cfg == nil {
39+
return fmt.Errorf("missing config")
40+
}
41+
42+
baseURL := serverBaseURL(cfg)
43+
fmt.Println("Restarting csghub-lite service...")
44+
if err := stopBackgroundServiceForRestart(); err != nil {
45+
return fmt.Errorf("stopping existing service: %w", err)
46+
}
47+
if err := startBackgroundServerForRestart(cfg); err != nil {
48+
return fmt.Errorf("starting service: %w", err)
49+
}
50+
if err := waitForServerForRestart(baseURL, 15*time.Second); err != nil {
51+
return err
52+
}
53+
fmt.Printf("Restarted csghub-lite service at %s\n", baseURL)
54+
return nil
55+
}

internal/cli/restart_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package cli
2+
3+
import (
4+
"errors"
5+
"reflect"
6+
"strings"
7+
"testing"
8+
"time"
9+
10+
"github.com/opencsgs/csghub-lite/internal/config"
11+
)
12+
13+
func TestRestartBackgroundServiceRestartsAndWaits(t *testing.T) {
14+
oldStop := stopBackgroundServiceForRestart
15+
oldStart := startBackgroundServerForRestart
16+
oldWait := waitForServerForRestart
17+
defer func() {
18+
stopBackgroundServiceForRestart = oldStop
19+
startBackgroundServerForRestart = oldStart
20+
waitForServerForRestart = oldWait
21+
}()
22+
23+
cfg := &config.Config{ListenAddr: ":14567"}
24+
var calls []string
25+
26+
stopBackgroundServiceForRestart = func() error {
27+
calls = append(calls, "stop")
28+
return nil
29+
}
30+
startBackgroundServerForRestart = func(got *config.Config) error {
31+
if got != cfg {
32+
t.Fatalf("start config = %#v, want original config", got)
33+
}
34+
calls = append(calls, "start")
35+
return nil
36+
}
37+
waitForServerForRestart = func(baseURL string, timeout time.Duration) error {
38+
if baseURL != "http://127.0.0.1:14567" {
39+
t.Fatalf("baseURL = %q, want http://127.0.0.1:14567", baseURL)
40+
}
41+
if timeout != 15*time.Second {
42+
t.Fatalf("timeout = %s, want 15s", timeout)
43+
}
44+
calls = append(calls, "wait")
45+
return nil
46+
}
47+
48+
if err := restartBackgroundService(cfg); err != nil {
49+
t.Fatalf("restartBackgroundService returned error: %v", err)
50+
}
51+
52+
if want := []string{"stop", "start", "wait"}; !reflect.DeepEqual(calls, want) {
53+
t.Fatalf("calls = %#v, want %#v", calls, want)
54+
}
55+
}
56+
57+
func TestRestartBackgroundServiceWrapsStopError(t *testing.T) {
58+
oldStop := stopBackgroundServiceForRestart
59+
oldStart := startBackgroundServerForRestart
60+
oldWait := waitForServerForRestart
61+
defer func() {
62+
stopBackgroundServiceForRestart = oldStop
63+
startBackgroundServerForRestart = oldStart
64+
waitForServerForRestart = oldWait
65+
}()
66+
67+
stopBackgroundServiceForRestart = func() error {
68+
return errors.New("boom")
69+
}
70+
startBackgroundServerForRestart = func(*config.Config) error {
71+
t.Fatal("start should not be called after stop failure")
72+
return nil
73+
}
74+
waitForServerForRestart = func(string, time.Duration) error {
75+
t.Fatal("wait should not be called after stop failure")
76+
return nil
77+
}
78+
79+
err := restartBackgroundService(&config.Config{ListenAddr: config.DefaultListenAddr})
80+
if err == nil || !strings.Contains(err.Error(), "stopping existing service: boom") {
81+
t.Fatalf("error = %v, want wrapped stop error", err)
82+
}
83+
}

internal/cli/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ and the full CSGHub platform.`,
3131
newPsCmd(),
3232
newStopCmd(),
3333
newStopServiceCmd(),
34+
newRestartCmd(),
3435
newRmCmd(),
3536
newLoginCmd(),
3637
newSearchCmd(),

0 commit comments

Comments
 (0)