@yufengtadian/freedom-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -0
- package/bin/freedom.js +11 -0
- package/lib/build.js +173 -0
- package/lib/cli.js +145 -0
- package/lib/config.js +68 -0
- package/lib/init.js +43 -0
- package/lib/utils.js +70 -0
- package/package.json +27 -0
- package/postinstall.js +36 -0
- package/templates/go/gen_config.go +19 -0
- package/templates/go/go.mod +5 -0
- package/templates/go/main.go +12 -0
- package/templates/go/pkg/freedom/assets/freedom.js +97 -0
- package/templates/go/pkg/freedom/assets_embed.go +18 -0
- package/templates/go/pkg/freedom/backend.go +21 -0
- package/templates/go/pkg/freedom/backend_proc.go +250 -0
- package/templates/go/pkg/freedom/bridge.go +129 -0
- package/templates/go/pkg/freedom/center_other.go +7 -0
- package/templates/go/pkg/freedom/center_windows.go +42 -0
- package/templates/go/pkg/freedom/freedom.go +257 -0
- package/templates/go/pkg/freedom/proc_hide_other.go +8 -0
- package/templates/go/pkg/freedom/proc_hide_windows.go +15 -0
- package/templates/go/pkg/freedom/window_other.go +25 -0
- package/templates/go/pkg/freedom/window_windows.go +133 -0
- package/templates/project/freedom.config.js +42 -0
- package/templates/project/index.html +82 -0
- package/templates/project/package.json +14 -0
- package/templates/project/src/main.js +26 -0
- package/templates/project/vite.config.js +18 -0
- package/tutorial/tutorial.html +81 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import "freedom-cli-shell/pkg/freedom"
|
|
4
|
+
|
|
5
|
+
// appConfig 返回应用窗口配置。
|
|
6
|
+
// 本文件由 freedom CLI 在 build 阶段根据 freedom.config.js 重新生成,
|
|
7
|
+
// 不要手动修改(手动修改会在下次 build 时被覆盖)。
|
|
8
|
+
func appConfig() freedom.Config {
|
|
9
|
+
return freedom.Config{
|
|
10
|
+
Title: "Freedom App",
|
|
11
|
+
TitleBar: freedom.TitleBarNative,
|
|
12
|
+
Width: 1024,
|
|
13
|
+
Height: 720,
|
|
14
|
+
MinWidth: 400,
|
|
15
|
+
MinHeight: 300,
|
|
16
|
+
Center: true,
|
|
17
|
+
Debug: false,
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import "freedom-cli-shell/pkg/freedom"
|
|
4
|
+
|
|
5
|
+
// main 是 Freedom 壳层应用入口。
|
|
6
|
+
// 窗口与标题栏配置来自 build 阶段生成的 gen_config.go(appConfig),
|
|
7
|
+
// 前端页面由 pkg/freedom 的 assets_embed.go 嵌入(build 阶段把打包后的 index.html 放进去)。
|
|
8
|
+
func main() {
|
|
9
|
+
cfg := appConfig()
|
|
10
|
+
app := freedom.New(cfg)
|
|
11
|
+
app.Run()
|
|
12
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Freedom 前端 SDK。
|
|
2
|
+
// window.__freedom_bridge 由 go-webview2 的 Bind 机制注册(异步桥接函数,返回 Promise),
|
|
3
|
+
// 注入脚本与 SDK 的执行先后顺序不固定,因此 SDK 采用"每次调用时动态读取"策略,
|
|
4
|
+
// 保证与注入顺序无关。
|
|
5
|
+
(function () {
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
function getBridge() {
|
|
9
|
+
return (typeof window.__freedom_bridge === 'function') ? window.__freedom_bridge : null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
var listeners = {};
|
|
13
|
+
|
|
14
|
+
function call(method) {
|
|
15
|
+
var params = Array.prototype.slice.call(arguments, 1);
|
|
16
|
+
var bridge = getBridge();
|
|
17
|
+
if (!bridge) {
|
|
18
|
+
return Promise.reject(new Error('[freedom] 未检测到原生桥接(__freedom_bridge),当前不在桌面壳内运行。'));
|
|
19
|
+
}
|
|
20
|
+
return bridge(method, JSON.stringify(params));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function on(event, cb) {
|
|
24
|
+
if (typeof cb !== 'function') return function () {};
|
|
25
|
+
(listeners[event] = listeners[event] || []).push(cb);
|
|
26
|
+
return function () { off(event, cb); };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function off(event, cb) {
|
|
30
|
+
var l = listeners[event] || [];
|
|
31
|
+
var i = l.indexOf(cb);
|
|
32
|
+
if (i >= 0) l.splice(i, 1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 后端通过 app.Emit(event, data) 触发(Eval 调用本函数)。
|
|
36
|
+
function emit(event, data) {
|
|
37
|
+
var l = listeners[event] || [];
|
|
38
|
+
for (var i = 0; i < l.length; i++) {
|
|
39
|
+
try { l[i](data); } catch (e) { /* 监听器异常不影响其余监听器 */ }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
var freedom = {
|
|
44
|
+
call: call,
|
|
45
|
+
invoke: call,
|
|
46
|
+
on: on,
|
|
47
|
+
off: off,
|
|
48
|
+
emit: emit,
|
|
49
|
+
window: {
|
|
50
|
+
// 窗口控制(无边框/隐藏标题栏模式下前端自绘按钮使用)。
|
|
51
|
+
// 全部经桥接调用原生实现,返回 Promise。
|
|
52
|
+
minimize: function () { return windowAction('minimize'); },
|
|
53
|
+
maximize: function () { return windowAction('maximize'); },
|
|
54
|
+
unmaximize: function () { return windowAction('unmaximize'); },
|
|
55
|
+
toggleMaximize: function () { return windowAction('toggleMaximize'); },
|
|
56
|
+
close: function () { return windowAction('close'); },
|
|
57
|
+
isMaximized: function () { return windowAction('isMaximized'); },
|
|
58
|
+
isFrameless: function () { return windowAction('isFrameless'); },
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// 前端自绘按钮的便捷绑定:把按钮 DOM 接到窗口控制。
|
|
63
|
+
// freedom.window.bindButtons({ min: '#minBtn', max: '#maxBtn', close: '#closeBtn' })
|
|
64
|
+
freedom.window.bindButtons = function (sel) {
|
|
65
|
+
var q = function (s) { return typeof s === 'string' ? document.querySelector(s) : s; };
|
|
66
|
+
var min = q(sel && sel.min), max = q(sel && sel.max), close = q(sel && sel.close);
|
|
67
|
+
var self = this;
|
|
68
|
+
if (min) min.addEventListener('click', function () { self.minimize(); });
|
|
69
|
+
if (max) max.addEventListener('click', function () {
|
|
70
|
+
self.isMaximized().then(function (m) {
|
|
71
|
+
if (m) self.unmaximize(); else self.maximize();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
if (close) close.addEventListener('click', function () { self.close(); });
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
function windowAction(action) {
|
|
78
|
+
// __freedom_window 是 webview_go Bind 注入的异步全局函数(返回 Promise)
|
|
79
|
+
if (typeof window.__freedom_window !== 'function') {
|
|
80
|
+
return Promise.reject(new Error('[freedom] 未检测到原生窗口控制桥接。'));
|
|
81
|
+
}
|
|
82
|
+
return window.__freedom_window(action);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
Object.defineProperty(freedom, 'isDesktop', {
|
|
86
|
+
get: function () { return !!getBridge(); },
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// 兼容 Wails 风格:window.go 对象,便于迁移既有代码。
|
|
90
|
+
var go = {};
|
|
91
|
+
Object.defineProperty(go, 'backend', {
|
|
92
|
+
get: function () { return { call: call, invoke: call }; },
|
|
93
|
+
});
|
|
94
|
+
window.go = go;
|
|
95
|
+
|
|
96
|
+
window.freedom = freedom;
|
|
97
|
+
})();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
package freedom
|
|
2
|
+
|
|
3
|
+
import _ "embed"
|
|
4
|
+
|
|
5
|
+
// jsSDK 是注入到每个前端页面的 Freedom 前端 SDK,暴露 window.freedom 全局对象。
|
|
6
|
+
// 用法见 assets/freedom.js 顶部注释。
|
|
7
|
+
//
|
|
8
|
+
//go:embed assets/freedom.js
|
|
9
|
+
var jsSDK string
|
|
10
|
+
|
|
11
|
+
// indexHTML 是前端打包产物(Vite 单文件 HTML),build 阶段写入 assets/index.html,
|
|
12
|
+
// 运行时通过 SetHtml 从内存加载。
|
|
13
|
+
//
|
|
14
|
+
//go:embed assets/index.html
|
|
15
|
+
var indexHTML string
|
|
16
|
+
|
|
17
|
+
// defaultHTML 兼容框架默认占位页语义:无自定义前端时回退到 indexHTML。
|
|
18
|
+
var defaultHTML = indexHTML
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
package freedom
|
|
2
|
+
|
|
3
|
+
import "encoding/json"
|
|
4
|
+
|
|
5
|
+
// Backend 是 Freedom 的后端抽象。一个 Freedom 应用在任意时刻绑定一个后端,
|
|
6
|
+
// 前端的所有 window.freedom.call(...) 最终都路由到该后端的 Handle。
|
|
7
|
+
//
|
|
8
|
+
// Freedom 内置两种后端实现:
|
|
9
|
+
// - EmbedBackend:Go 方法直接注册在壳进程内(传统模式,app.Bind 使用)。
|
|
10
|
+
// - ProcBackend:后端是任意语言实现的独立进程,通过 stdio NDJSON/JSON-RPC
|
|
11
|
+
// 与壳通信。这是"后端任意语言 + 三平台"的核心能力。
|
|
12
|
+
type Backend interface {
|
|
13
|
+
// Handle 处理一次前端调用。method 是方法名,params 是参数列表的 JSON 编码
|
|
14
|
+
// 数组(每个元素是一个 json.RawMessage)。
|
|
15
|
+
// 返回值 result 会经 JSON 序列化后回传前端 Promise 的 resolve;
|
|
16
|
+
// 返回非 nil error 时前端 Promise 走 reject。
|
|
17
|
+
Handle(method string, params []json.RawMessage) (interface{}, error)
|
|
18
|
+
|
|
19
|
+
// Close 释放后端资源(内嵌后端为空操作;进程后端会终止子进程)。
|
|
20
|
+
Close() error
|
|
21
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
package freedom
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bufio"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"fmt"
|
|
7
|
+
"io"
|
|
8
|
+
"os"
|
|
9
|
+
"os/exec"
|
|
10
|
+
"sync"
|
|
11
|
+
"time"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
// ProcBackend 使用任意语言实现的独立后端进程。
|
|
15
|
+
//
|
|
16
|
+
// 通信协议(语言无关,换行分隔 JSON / NDJSON,走 stdin/stdout):
|
|
17
|
+
//
|
|
18
|
+
// 壳 -> 后端(写 stdin):
|
|
19
|
+
// {"id":1,"method":"Greet","params":["老板"]}
|
|
20
|
+
// 后端 -> 壳(写 stdout):
|
|
21
|
+
// {"id":1,"result":{...}} 调用成功响应(result 可为任意 JSON 或 null)
|
|
22
|
+
// {"id":1,"error":"boom"} 调用失败响应
|
|
23
|
+
// {"event":"tick","data":{...}} 主动事件推送(无 id 字段)
|
|
24
|
+
//
|
|
25
|
+
// 后端的 stderr 仅用于人类日志,壳原样转发到控制台,不参与协议。
|
|
26
|
+
//
|
|
27
|
+
// 壳启动后端进程时注入环境变量:FREEDOM_BACKEND=1、FREEDOM_IPC=stdio,
|
|
28
|
+
// 后端可据此判断自己运行在 Freedom 壳内。
|
|
29
|
+
type ProcBackend struct {
|
|
30
|
+
cmd *exec.Cmd
|
|
31
|
+
stdin io.WriteCloser
|
|
32
|
+
mu sync.Mutex
|
|
33
|
+
pending map[int64]chan procResp
|
|
34
|
+
nextID int64
|
|
35
|
+
closed bool
|
|
36
|
+
onEvent func(event string, data interface{})
|
|
37
|
+
timeout time.Duration
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// procResp 是一次调用在壳侧的等待结果。
|
|
41
|
+
type procResp struct {
|
|
42
|
+
result json.RawMessage
|
|
43
|
+
err error
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// procMessage 是协议的统一消息结构。
|
|
47
|
+
type procMessage struct {
|
|
48
|
+
ID int64 `json:"id,omitempty"`
|
|
49
|
+
Method string `json:"method,omitempty"`
|
|
50
|
+
Params json.RawMessage `json:"params,omitempty"`
|
|
51
|
+
Event string `json:"event,omitempty"`
|
|
52
|
+
Data json.RawMessage `json:"data,omitempty"`
|
|
53
|
+
Result json.RawMessage `json:"result,omitempty"`
|
|
54
|
+
Error string `json:"error,omitempty"`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// NewProcBackend 创建一个进程后端。command 是任意语言后端可执行文件的启动命令
|
|
58
|
+
// (如 "node"、"python"、"./backend.py" 或可执行文件绝对路径),后续参数原样透传。
|
|
59
|
+
// 返回的 ProcBackend 在 App.Run 启动窗口时自动拉起后端进程。
|
|
60
|
+
func NewProcBackend(command ...string) *ProcBackend {
|
|
61
|
+
if len(command) == 0 {
|
|
62
|
+
panic("freedom: NewProcBackend requires at least one command argument")
|
|
63
|
+
}
|
|
64
|
+
cmd := exec.Command(command[0], command[1:]...)
|
|
65
|
+
hideWindow(cmd) // Windows 下隐藏后端的 cmd 黑窗(跨平台空实现)
|
|
66
|
+
return &ProcBackend{
|
|
67
|
+
cmd: cmd,
|
|
68
|
+
pending: map[int64]chan procResp{},
|
|
69
|
+
timeout: 60 * time.Second,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// SetTimeout 设置单次调用的最大等待时间(默认 60s)。<=0 表示不超时。
|
|
74
|
+
func (p *ProcBackend) SetTimeout(d time.Duration) *ProcBackend {
|
|
75
|
+
p.timeout = d
|
|
76
|
+
return p
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// OnEvent 注册后端进程推送事件时的回调(由框架在 Run 时注入)。
|
|
80
|
+
func (p *ProcBackend) OnEvent(fn func(event string, data interface{})) {
|
|
81
|
+
p.mu.Lock()
|
|
82
|
+
p.onEvent = fn
|
|
83
|
+
p.mu.Unlock()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// start 启动后端进程并开始读取其 stdout。幂等,可从任意 goroutine 调用。
|
|
87
|
+
func (p *ProcBackend) start() error {
|
|
88
|
+
p.mu.Lock()
|
|
89
|
+
defer p.mu.Unlock()
|
|
90
|
+
if p.stdin != nil {
|
|
91
|
+
return nil // 已启动
|
|
92
|
+
}
|
|
93
|
+
p.cmd.Env = append(os.Environ(), "FREEDOM_BACKEND=1", "FREEDOM_IPC=stdio")
|
|
94
|
+
p.cmd.Stderr = os.Stderr // 后端 stderr 日志原样转发
|
|
95
|
+
stdin, err := p.cmd.StdinPipe()
|
|
96
|
+
if err != nil {
|
|
97
|
+
return fmt.Errorf("freedom: proc backend stdin pipe: %w", err)
|
|
98
|
+
}
|
|
99
|
+
stdout, err := p.cmd.StdoutPipe()
|
|
100
|
+
if err != nil {
|
|
101
|
+
return fmt.Errorf("freedom: proc backend stdout pipe: %w", err)
|
|
102
|
+
}
|
|
103
|
+
if err := p.cmd.Start(); err != nil {
|
|
104
|
+
return fmt.Errorf("freedom: proc backend start %q: %w", p.cmd.Path, err)
|
|
105
|
+
}
|
|
106
|
+
p.stdin = stdin
|
|
107
|
+
go p.readLoop(stdout)
|
|
108
|
+
return nil
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Handle 实现 Backend 接口:把一次前端调用转发给后端进程并等待其响应。
|
|
112
|
+
func (p *ProcBackend) Handle(method string, params []json.RawMessage) (interface{}, error) {
|
|
113
|
+
paramsJSON, err := json.Marshal(params)
|
|
114
|
+
if err != nil {
|
|
115
|
+
return nil, fmt.Errorf("freedom: proc backend: marshal params: %w", err)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
p.mu.Lock()
|
|
119
|
+
if p.closed || p.stdin == nil {
|
|
120
|
+
p.mu.Unlock()
|
|
121
|
+
return nil, fmt.Errorf("freedom: proc backend is not running")
|
|
122
|
+
}
|
|
123
|
+
p.nextID++
|
|
124
|
+
id := p.nextID
|
|
125
|
+
ch := make(chan procResp, 1)
|
|
126
|
+
p.pending[id] = ch
|
|
127
|
+
msg := procMessage{ID: id, Method: method, Params: paramsJSON}
|
|
128
|
+
line, _ := json.Marshal(msg)
|
|
129
|
+
_, err = p.stdin.Write(append(line, '\n'))
|
|
130
|
+
p.mu.Unlock()
|
|
131
|
+
|
|
132
|
+
if err != nil {
|
|
133
|
+
p.cancel(id, fmt.Errorf("freedom: proc backend write: %w", err))
|
|
134
|
+
return nil, err
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
var resp procResp
|
|
138
|
+
if p.timeout > 0 {
|
|
139
|
+
select {
|
|
140
|
+
case resp = <-ch:
|
|
141
|
+
case <-time.After(p.timeout):
|
|
142
|
+
p.cancel(id, fmt.Errorf("freedom: proc backend: method %q timed out after %s", method, p.timeout))
|
|
143
|
+
return nil, fmt.Errorf("freedom: proc backend: method %q timed out after %s", method, p.timeout)
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
resp = <-ch
|
|
147
|
+
}
|
|
148
|
+
if resp.err != nil {
|
|
149
|
+
return nil, resp.err
|
|
150
|
+
}
|
|
151
|
+
if resp.result == nil {
|
|
152
|
+
return nil, nil
|
|
153
|
+
}
|
|
154
|
+
var out interface{}
|
|
155
|
+
if err := json.Unmarshal(resp.result, &out); err != nil {
|
|
156
|
+
return nil, fmt.Errorf("freedom: proc backend: bad result: %w", err)
|
|
157
|
+
}
|
|
158
|
+
return out, nil
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// cancel 移除并唤醒一个等待中的调用(超时 / 写失败 / 进程退出)。
|
|
162
|
+
func (p *ProcBackend) cancel(id int64, err error) {
|
|
163
|
+
p.mu.Lock()
|
|
164
|
+
if ch, ok := p.pending[id]; ok {
|
|
165
|
+
delete(p.pending, id)
|
|
166
|
+
ch <- procResp{err: err}
|
|
167
|
+
}
|
|
168
|
+
p.mu.Unlock()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// readLoop 持续读取后端 stdout,解析协议消息并分发。
|
|
172
|
+
func (p *ProcBackend) readLoop(stdout io.Reader) {
|
|
173
|
+
sc := bufio.NewScanner(stdout)
|
|
174
|
+
sc.Buffer(make([]byte, 0, 64*1024), 16*1024*1024)
|
|
175
|
+
for sc.Scan() {
|
|
176
|
+
var msg procMessage
|
|
177
|
+
if err := json.Unmarshal(sc.Bytes(), &msg); err != nil {
|
|
178
|
+
fmt.Fprintf(os.Stderr, "freedom: proc backend: bad message: %v\n", err)
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
if msg.Event != "" {
|
|
182
|
+
p.dispatchEvent(msg.Event, msg.Data)
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
185
|
+
p.finish(msg)
|
|
186
|
+
}
|
|
187
|
+
// 后端进程已退出:唤醒所有仍等待中的调用。
|
|
188
|
+
err := fmt.Errorf("freedom: proc backend exited unexpectedly")
|
|
189
|
+
p.mu.Lock()
|
|
190
|
+
for id, ch := range p.pending {
|
|
191
|
+
delete(p.pending, id)
|
|
192
|
+
ch <- procResp{err: err}
|
|
193
|
+
}
|
|
194
|
+
p.mu.Unlock()
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
func (p *ProcBackend) finish(msg procMessage) {
|
|
198
|
+
p.mu.Lock()
|
|
199
|
+
ch, ok := p.pending[msg.ID]
|
|
200
|
+
delete(p.pending, msg.ID)
|
|
201
|
+
p.mu.Unlock()
|
|
202
|
+
if !ok {
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
if msg.Error != "" {
|
|
206
|
+
ch <- procResp{err: fmt.Errorf("%s", msg.Error)}
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
ch <- procResp{result: msg.Result}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
func (p *ProcBackend) dispatchEvent(event string, dataJSON json.RawMessage) {
|
|
213
|
+
var data interface{}
|
|
214
|
+
if len(dataJSON) > 0 {
|
|
215
|
+
_ = json.Unmarshal(dataJSON, &data)
|
|
216
|
+
}
|
|
217
|
+
p.mu.Lock()
|
|
218
|
+
fn := p.onEvent
|
|
219
|
+
p.mu.Unlock()
|
|
220
|
+
if fn != nil {
|
|
221
|
+
fn(event, data)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Close 终止后端进程。先关闭 stdin 通知其优雅退出,超时则强杀。线程安全。
|
|
226
|
+
func (p *ProcBackend) Close() error {
|
|
227
|
+
p.mu.Lock()
|
|
228
|
+
if p.closed {
|
|
229
|
+
p.mu.Unlock()
|
|
230
|
+
return nil
|
|
231
|
+
}
|
|
232
|
+
p.closed = true
|
|
233
|
+
cmd := p.cmd
|
|
234
|
+
if p.stdin != nil {
|
|
235
|
+
_ = p.stdin.Close() // stdin EOF,后端可自行退出
|
|
236
|
+
}
|
|
237
|
+
p.mu.Unlock()
|
|
238
|
+
|
|
239
|
+
done := make(chan struct{})
|
|
240
|
+
go func() { _ = cmd.Wait(); close(done) }()
|
|
241
|
+
select {
|
|
242
|
+
case <-done:
|
|
243
|
+
case <-time.After(3 * time.Second):
|
|
244
|
+
if cmd.Process != nil {
|
|
245
|
+
_ = cmd.Process.Kill()
|
|
246
|
+
<-done
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return nil
|
|
250
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
package freedom
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"fmt"
|
|
6
|
+
"reflect"
|
|
7
|
+
"sync"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
var errorType = reflect.TypeOf((*error)(nil)).Elem()
|
|
11
|
+
|
|
12
|
+
// EmbedBackend 是把 Go 方法集合直接注册在壳进程内的后端(传统模式)。
|
|
13
|
+
// 前端调用 window.freedom.call(name, ...args) 时,桥接层按方法签名反射调用。
|
|
14
|
+
type EmbedBackend struct {
|
|
15
|
+
methods map[string]reflect.Value
|
|
16
|
+
mu sync.RWMutex
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// NewEmbedBackend 创建一个内嵌 Go 后端。
|
|
20
|
+
func NewEmbedBackend() *EmbedBackend {
|
|
21
|
+
return &EmbedBackend{methods: map[string]reflect.Value{}}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Bind 把后端 Go 方法暴露给前端。
|
|
25
|
+
//
|
|
26
|
+
// - fn 必须是函数。
|
|
27
|
+
// - 参数与返回值通过 JSON 编解码,前端用 window.freedom.call(name, ...args) 调用,
|
|
28
|
+
// 返回 Promise(resolve 为返回值,reject 为 error 的字符串表示)。
|
|
29
|
+
// - 返回值约定:可返回 (T, error) 或仅 (T) 或仅 error 或空。
|
|
30
|
+
func (b *EmbedBackend) Bind(name string, fn interface{}) error {
|
|
31
|
+
v := reflect.ValueOf(fn)
|
|
32
|
+
if v.Kind() != reflect.Func {
|
|
33
|
+
return fmt.Errorf("freedom: Bind(%q): only functions can be bound", name)
|
|
34
|
+
}
|
|
35
|
+
if n := v.Type().NumOut(); n > 2 {
|
|
36
|
+
return fmt.Errorf("freedom: Bind(%q): function may return at most a value and an error", name)
|
|
37
|
+
}
|
|
38
|
+
b.mu.Lock()
|
|
39
|
+
b.methods[name] = v
|
|
40
|
+
b.mu.Unlock()
|
|
41
|
+
return nil
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Unbind 移除先前 Bind 的方法。
|
|
45
|
+
func (b *EmbedBackend) Unbind(name string) {
|
|
46
|
+
b.mu.Lock()
|
|
47
|
+
delete(b.methods, name)
|
|
48
|
+
b.mu.Unlock()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Handle 实现 Backend 接口:按方法名反射调用已绑定的 Go 函数。
|
|
52
|
+
func (b *EmbedBackend) Handle(method string, params []json.RawMessage) (interface{}, error) {
|
|
53
|
+
b.mu.RLock()
|
|
54
|
+
fn, ok := b.methods[method]
|
|
55
|
+
b.mu.RUnlock()
|
|
56
|
+
if !ok {
|
|
57
|
+
return nil, fmt.Errorf("freedom: method %q is not bound", method)
|
|
58
|
+
}
|
|
59
|
+
return callFunction(fn, params)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Close 内嵌后端没有需要释放的资源。
|
|
63
|
+
func (b *EmbedBackend) Close() error { return nil }
|
|
64
|
+
|
|
65
|
+
// callFunction 反射调用一个已绑定的 Go 函数。
|
|
66
|
+
// 返回值约定:0 个(忽略)、1 个(值或 error)、2 个(值, error)。
|
|
67
|
+
func callFunction(fn reflect.Value, params []json.RawMessage) (interface{}, error) {
|
|
68
|
+
t := fn.Type()
|
|
69
|
+
|
|
70
|
+
if t.IsVariadic() {
|
|
71
|
+
fixed := t.NumIn() - 1
|
|
72
|
+
if len(params) < fixed {
|
|
73
|
+
return nil, fmt.Errorf("freedom: arg count mismatch: want >=%d got %d", fixed, len(params))
|
|
74
|
+
}
|
|
75
|
+
args := make([]reflect.Value, len(params))
|
|
76
|
+
for i, p := range params {
|
|
77
|
+
var typ reflect.Type
|
|
78
|
+
if i < fixed {
|
|
79
|
+
typ = t.In(i)
|
|
80
|
+
} else {
|
|
81
|
+
typ = t.In(fixed).Elem()
|
|
82
|
+
}
|
|
83
|
+
ptr := reflect.New(typ)
|
|
84
|
+
if err := json.Unmarshal(p, ptr.Interface()); err != nil {
|
|
85
|
+
return nil, fmt.Errorf("freedom: arg %d: %w", i, err)
|
|
86
|
+
}
|
|
87
|
+
args[i] = ptr.Elem()
|
|
88
|
+
}
|
|
89
|
+
return collectResults(fn.Call(args))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if len(params) != t.NumIn() {
|
|
93
|
+
return nil, fmt.Errorf("freedom: arg count mismatch: want %d got %d", t.NumIn(), len(params))
|
|
94
|
+
}
|
|
95
|
+
args := make([]reflect.Value, len(params))
|
|
96
|
+
for i, p := range params {
|
|
97
|
+
ptr := reflect.New(t.In(i))
|
|
98
|
+
if err := json.Unmarshal(p, ptr.Interface()); err != nil {
|
|
99
|
+
return nil, fmt.Errorf("freedom: arg %d: %w", i, err)
|
|
100
|
+
}
|
|
101
|
+
args[i] = ptr.Elem()
|
|
102
|
+
}
|
|
103
|
+
return collectResults(fn.Call(args))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
func collectResults(res []reflect.Value) (interface{}, error) {
|
|
107
|
+
switch len(res) {
|
|
108
|
+
case 0:
|
|
109
|
+
return nil, nil
|
|
110
|
+
case 1:
|
|
111
|
+
if res[0].Type().Implements(errorType) {
|
|
112
|
+
if !res[0].IsNil() {
|
|
113
|
+
return nil, res[0].Interface().(error)
|
|
114
|
+
}
|
|
115
|
+
return nil, nil
|
|
116
|
+
}
|
|
117
|
+
return res[0].Interface(), nil
|
|
118
|
+
case 2:
|
|
119
|
+
if !res[1].Type().Implements(errorType) {
|
|
120
|
+
return nil, fmt.Errorf("freedom: second return value must be error")
|
|
121
|
+
}
|
|
122
|
+
if !res[1].IsNil() {
|
|
123
|
+
return res[0].Interface(), res[1].Interface().(error)
|
|
124
|
+
}
|
|
125
|
+
return res[0].Interface(), nil
|
|
126
|
+
default:
|
|
127
|
+
return nil, fmt.Errorf("freedom: function may return at most a value and an error")
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
//go:build windows
|
|
2
|
+
|
|
3
|
+
package freedom
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"syscall"
|
|
7
|
+
"unsafe"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
var (
|
|
11
|
+
user32 = syscall.NewLazyDLL("user32.dll")
|
|
12
|
+
getSystemMetrics = user32.NewProc("GetSystemMetrics")
|
|
13
|
+
moveWindow = user32.NewProc("MoveWindow")
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
const (
|
|
17
|
+
smCxScreen = 16
|
|
18
|
+
smCyScreen = 17
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
// applyCenter 在 Windows 上把窗口置于屏幕中央(需在 SetSize 之后调用)。
|
|
22
|
+
// webview_go 未提供 SetPosition,这里通过原生 HWND + MoveWindow 定位。
|
|
23
|
+
func (a *App) applyCenter() {
|
|
24
|
+
if !a.cfg.Center || a.view == nil {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
hwnd := uintptr(unsafe.Pointer(a.view.Window()))
|
|
28
|
+
if hwnd == 0 {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
sw, _, _ := getSystemMetrics.Call(smCxScreen)
|
|
32
|
+
sh, _, _ := getSystemMetrics.Call(smCyScreen)
|
|
33
|
+
x := int(sw/2) - a.cfg.Width/2
|
|
34
|
+
y := int(sh/2) - a.cfg.Height/2
|
|
35
|
+
if x < 0 {
|
|
36
|
+
x = 0
|
|
37
|
+
}
|
|
38
|
+
if y < 0 {
|
|
39
|
+
y = 0
|
|
40
|
+
}
|
|
41
|
+
moveWindow.Call(hwnd, uintptr(x), uintptr(y), uintptr(a.cfg.Width), uintptr(a.cfg.Height), 1)
|
|
42
|
+
}
|