仰望星辰工作室

clearlove2.1

clearlove2.1/ internal/plugin/lifecycle.go 5.1 KB · 176 行 原始文件
1// 应用型插件生命周期:加载(setup 注册)、卸载、启动时批量引导。
2package plugin
3
4import (
5 "context"
6 "errors"
7 "fmt"
8 "os"
9 "path/filepath"
10 "strings"
11 "time"
12
13 "clearlove/internal/util"
14)
15
16// Boot 启动时加载全部已启用的应用型插件(A 型插件无需加载,按请求即时解析)
17func Boot() {
18 for name, p := range Load() {
19 if !p.IsApp() {
20 continue
21 }
22 if !enabledSet()[p.Name] {
23 continue
24 }
25 if err := LoadApp(name); err != nil {
26 util.Log("error", "应用型插件 %s 加载失败: %v", name, err)
27 logPlugin(p.SlugOf(), "load_error", err.Error(), 0)
28 }
29 }
30}
31
32// LoadApp 加载并启动一个应用型插件(幂等:重复调用会先卸载)
33func LoadApp(dir string) error {
34 dir = filepath.Base(strings.TrimSpace(dir))
35 if dir == "" || dir == "." {
36 return errors.New("无效的插件目录")
37 }
38 full := filepath.Join(dir_pluginsDir(), dir)
39 data, err := os.ReadFile(filepath.Join(full, "plugin.json"))
40 if err != nil {
41 return fmt.Errorf("读取 plugin.json 失败: %w", err)
42 }
43 var p Plugin
44 if err := unmarshalPlugin(data, &p); err != nil {
45 return err
46 }
47 if !p.IsApp() {
48 return errors.New("该插件不是应用型(kind != app),无需加载运行时")
49 }
50 if p.Runtime == nil || strings.TrimSpace(p.Runtime.Engine) == "" {
51 return errors.New("缺少 runtime.engine(当前支持 js)")
52 }
53 if !strings.EqualFold(strings.TrimSpace(p.Runtime.Engine), "js") {
54 return fmt.Errorf("不支持的运行时引擎 %q(当前仅支持 js)", p.Runtime.Engine)
55 }
56
57 // 幂等:先卸载旧实例
58 UnloadApp(dir)
59
60 rt, err := newJSRuntime(full, &p)
61 if err != nil {
62 return err
63 }
64 worker := NewWorker(rt)
65 app := newApp(dir, &p, worker)
66 rt.app = app // Host API 用它做权限校验与注册
67
68 // 1) 执行入口脚本(顶层只做定义)
69 ctx, cancel := context.WithTimeout(context.Background(), p.Timeout())
70 err = loadEntryWith(worker, ctx, rt)
71 cancel()
72 if err != nil {
73 worker.Close()
74 return err
75 }
76
77 // 2) 生命周期:安装回调(仅当标记为首次)
78 ctx2, cancel2 := context.WithTimeout(context.Background(), p.Timeout())
79 callOptional(worker, ctx2, rt, "onInstall")
80 cancel2()
81
82 // 3) setup:注册全部扩展点
83 setupCtx, cancelSetup := context.WithTimeout(context.Background(), setupTimeoutBudget(&p))
84 err = runSetup(worker, setupCtx, rt)
85 cancelSetup()
86 if err != nil {
87 worker.Close()
88 return fmt.Errorf("setup() 执行失败: %w", err)
89 }
90
91 // 4) 生成注册表快照并启动任务
92 RegisterApp(app)
93 startJobs(app)
94 noteSuccess(app)
95 logPlugin(p.SlugOf(), "enable", "插件已加载", 0)
96 util.Log("info", "应用型插件 %s(%s) 已加载:路由 %d、后台页 %d、菜单 %d、slot %d、事件 %d、过滤器 %d、任务 %d",
97 p.Name, p.SlugOf(), len(app.Routes), len(app.Pages), len(app.Menus),
98 len(app.Slots), len(app.Events), len(app.Filters), len(app.Jobs))
99 return nil
100}
101
102// UnloadApp 停止并移除一个应用型插件
103func UnloadApp(dir string) {
104 dir = filepath.Base(strings.TrimSpace(dir))
105 if dir == "" || dir == "." {
106 return
107 }
108 if a := GetApp(dir); a != nil {
109 ctx, cancel := context.WithTimeout(context.Background(), a.Plugin.Timeout())
110 callOptional(a.Worker, ctx, a.Worker.rt, "onDisable")
111 cancel()
112 stopJobs(dir)
113 a.Worker.Close()
114 logPlugin(a.Plugin.SlugOf(), "disable", "插件已卸载", 0)
115 }
116 UnregisterApp(dir)
117}
118
119// ReloadApp 重载(用于开发调试:修改脚本后重新 setup)
120func ReloadApp(dir string) error {
121 UnloadApp(dir)
122 return LoadApp(dir)
123}
124
125// loadEntryWith 在 worker 中执行入口脚本
126func loadEntryWith(w *Worker, ctx context.Context, rt *jsRuntime) error {
127 _, err := w.Do(ctx, func(r *jsRuntime) (any, error) {
128 return nil, r.loadEntry()
129 })
130 return err
131}
132
133// runSetup 在 worker 中执行 setup()
134func runSetup(w *Worker, ctx context.Context, rt *jsRuntime) error {
135 _, err := w.Do(ctx, func(r *jsRuntime) (any, error) {
136 if !r.hasFn("setup") {
137 return nil, errors.New("应用型插件必须定义 setup() 函数")
138 }
139 _, err := r.callFn("setup")
140 return nil, err
141 })
142 return err
143}
144
145// callOptional 调用可选的生命周期回调(未定义或出错都不影响主流程)
146func callOptional(w *Worker, ctx context.Context, rt *jsRuntime, fn string) {
147 if w == nil || rt == nil {
148 return
149 }
150 _, err := w.Do(ctx, func(r *jsRuntime) (any, error) {
151 if !r.hasFn(fn) {
152 return nil, nil
153 }
154 _, err := r.callFn(fn)
155 return nil, err
156 })
157 if err != nil && !errors.Is(err, context.DeadlineExceeded) {
158 util.Log("warn", "插件回调 %s 执行失败: %v", fn, err)
159 }
160}
161
162// setupTimeoutBudget setup 允许稍长一点的时间,但不应像运行时那样放长
163// (初始化只做注册,耗时的网络调用应放在路由/任务里)
164func setupTimeoutBudget(p *Plugin) time.Duration {
165 d := p.Timeout()
166 if d > time.Duration(setupMaxMS)*time.Millisecond {
167 return time.Duration(setupMaxMS) * time.Millisecond
168 }
169 if d < time.Duration(setupTimeoutMS)*time.Millisecond {
170 return time.Duration(setupTimeoutMS) * time.Millisecond
171 }
172 return d
173}
174
175// dir_pluginsDir 插件根目录(跟随数据目录配置)
176func dir_pluginsDir() string { return dir() }