@lazycatcloud/lzc-cli 1.2.37 → 1.2.41
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 +2 -9
- package/lib/app/apkshell.js +5 -3
- package/lib/app/lpk_build.js +12 -7
- package/lib/app/lpk_create_generator.js +4 -4
- package/lib/app/lpk_debug_bridge.js +83 -29
- package/lib/app/lpk_devshell.js +82 -41
- package/lib/shellapi.js +57 -20
- package/lib/utils.js +101 -13
- package/package.json +3 -1
- package/template/_lpk/busybox-1.35.0 +0 -0
- package/template/_lpk/exec.sh +1 -1
- package/template/_lpk/init_debug_bridge.sh +15 -7
- package/template/_lpk/rsyncd.conf +28 -0
- package/template/_lpk/win-rsync/msys-2.0.dll +0 -0
- package/template/_lpk/win-rsync/rsync.exe +0 -0
package/README.md
CHANGED
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
#### 依赖
|
|
4
4
|
|
|
5
5
|
1. `ssh`
|
|
6
|
-
2. `
|
|
7
|
-
3.
|
|
8
|
-
4. 盒子中安装开发者工具应用
|
|
6
|
+
2. `Linux` 和 `MacOS` 下需要安装 `rsync`
|
|
7
|
+
3. 盒子中安装开发者工具应用
|
|
9
8
|
|
|
10
9
|
#### 快速上手
|
|
11
10
|
|
|
@@ -32,9 +31,3 @@ lzc-cli project devshell
|
|
|
32
31
|
# 经过测试后,将包发布到懒猫云商店中去
|
|
33
32
|
lzc-cli appstore publish
|
|
34
33
|
```
|
|
35
|
-
|
|
36
|
-
### FAQ
|
|
37
|
-
|
|
38
|
-
#### 1. 开发者工具 ssh 的帐号密码是多少?
|
|
39
|
-
|
|
40
|
-
现在帐号密码设置为 box:box,后面会使用 pam 模块对接盒子帐号体系。
|
package/lib/app/apkshell.js
CHANGED
|
@@ -17,14 +17,16 @@ export async function triggerApk(id, name, iconPath) {
|
|
|
17
17
|
}
|
|
18
18
|
const resp = await api.post(
|
|
19
19
|
"https://appstore.lazycat.cloud/api/trigger_latest_for_app",
|
|
20
|
-
form
|
|
20
|
+
form,
|
|
21
|
+
{ timeout: 5000 }
|
|
21
22
|
)
|
|
22
23
|
if (resp.status == 304) {
|
|
23
|
-
|
|
24
|
+
logger.debug(`APK构建任务已创建成功,如需使用安卓端,请耐心等待1分钟左右`)
|
|
25
|
+
} else if (resp.status <= 201) {
|
|
24
26
|
logger.info(`APK构建任务已创建成功,如需使用安卓端,请耐心等待1分钟左右`)
|
|
25
27
|
} else if (resp.status >= 400) {
|
|
26
28
|
logger.debug("请求按钮应用出错:", resp)
|
|
27
|
-
throw
|
|
29
|
+
throw `请求生成应用出错! 使用 --apk=n 停止生成APK`
|
|
28
30
|
}
|
|
29
31
|
} catch (error) {
|
|
30
32
|
logger.error(error)
|
package/lib/app/lpk_build.js
CHANGED
|
@@ -9,9 +9,12 @@ import {
|
|
|
9
9
|
envTemplateFile,
|
|
10
10
|
isValidPackageName,
|
|
11
11
|
tarContentDir,
|
|
12
|
-
isPngWithFile
|
|
12
|
+
isPngWithFile,
|
|
13
|
+
isMacOs,
|
|
14
|
+
isWindows,
|
|
15
|
+
isLinux
|
|
13
16
|
} from "../utils.js"
|
|
14
|
-
import
|
|
17
|
+
import spawn from "cross-spawn"
|
|
15
18
|
import { LpkManifest } from "./lpk_create.js"
|
|
16
19
|
import archiver from "archiver"
|
|
17
20
|
import yaml from "js-yaml"
|
|
@@ -79,8 +82,8 @@ function localIp() {
|
|
|
79
82
|
const regex = /inet6 (fc03:1136:[0-9a-fA-F:]+)[?:\/ ]/
|
|
80
83
|
|
|
81
84
|
let output = ""
|
|
82
|
-
if (
|
|
83
|
-
const result =
|
|
85
|
+
if (isMacOs) {
|
|
86
|
+
const result = spawn.sync("sh", ["-c", "ifconfig"], {
|
|
84
87
|
encoding: "utf-8"
|
|
85
88
|
})
|
|
86
89
|
if (result.status != 0 || result.error) {
|
|
@@ -88,11 +91,11 @@ function localIp() {
|
|
|
88
91
|
return ""
|
|
89
92
|
}
|
|
90
93
|
output = result.stdout
|
|
91
|
-
} else if (
|
|
94
|
+
} else if (isWindows) {
|
|
92
95
|
logger.debug(`当前 Windows 不支持使用 LocalIP 反代`)
|
|
93
96
|
return ""
|
|
94
97
|
} else {
|
|
95
|
-
const result =
|
|
98
|
+
const result = spawn.sync("ip", ["addr", "show", "heiyu-0"], {
|
|
96
99
|
encoding: "utf-8"
|
|
97
100
|
})
|
|
98
101
|
if (result.status !== 0 || result.error) {
|
|
@@ -198,7 +201,9 @@ export class LpkBuild {
|
|
|
198
201
|
}
|
|
199
202
|
|
|
200
203
|
if (this.options["buildscript"]) {
|
|
201
|
-
|
|
204
|
+
const cmd = isWindows ? "cmd" : "sh"
|
|
205
|
+
const cmdArgs = isWindows ? "/c" : "-c"
|
|
206
|
+
let p = spawn.sync(cmd, [cmdArgs, this.options["buildscript"]], {
|
|
202
207
|
cwd: this.pwd,
|
|
203
208
|
stdio: "inherit"
|
|
204
209
|
})
|
|
@@ -4,7 +4,7 @@ import fs from "fs"
|
|
|
4
4
|
import { isBinaryFileSync } from "isbinaryfile"
|
|
5
5
|
import { contextDirname, isFileExist } from "../utils.js"
|
|
6
6
|
import chalk from "chalk"
|
|
7
|
-
import
|
|
7
|
+
import spawn from "cross-spawn"
|
|
8
8
|
import logger from "loglevel"
|
|
9
9
|
|
|
10
10
|
const __dirname = contextDirname(import.meta.url)
|
|
@@ -75,10 +75,10 @@ function vueTemplate(type) {
|
|
|
75
75
|
const v = type == "vue3" ? "vue@3" : "vue@2"
|
|
76
76
|
return {
|
|
77
77
|
build: async (to, option) => {
|
|
78
|
-
|
|
78
|
+
spawn.sync("npm", ["init", v, path.basename(to)], { stdio: "inherit" })
|
|
79
79
|
},
|
|
80
80
|
after: async function (name, cwd) {
|
|
81
|
-
|
|
81
|
+
spawn.sync("npm", ["install"], {
|
|
82
82
|
cwd: path.resolve(cwd, name),
|
|
83
83
|
stdio: "inherit"
|
|
84
84
|
})
|
|
@@ -182,7 +182,7 @@ export const TemplateConfig = {
|
|
|
182
182
|
appendREADME(path.resolve(cwd, name))
|
|
183
183
|
|
|
184
184
|
console.log(chalk.green("开始安装依赖"))
|
|
185
|
-
|
|
185
|
+
spawn.sync("npm", ["install"], {
|
|
186
186
|
cwd: path.join(cwd, name),
|
|
187
187
|
stdio: "inherit"
|
|
188
188
|
})
|
|
@@ -1,26 +1,48 @@
|
|
|
1
|
-
import
|
|
1
|
+
import spawn from "cross-spawn"
|
|
2
2
|
import fs from "node:fs"
|
|
3
3
|
import shellApi from "../shellapi.js"
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
isDebugMode,
|
|
6
|
+
isTraceMode,
|
|
7
|
+
resolveDomain,
|
|
8
|
+
sleep,
|
|
9
|
+
findSshPublicKey,
|
|
10
|
+
isWindows,
|
|
11
|
+
contextDirname
|
|
12
|
+
} from "../utils.js"
|
|
5
13
|
import logger from "loglevel"
|
|
6
14
|
import commandExists from "command-exists"
|
|
15
|
+
import path from "node:path"
|
|
16
|
+
|
|
17
|
+
export function sshBinary() {
|
|
18
|
+
if (isWindows) {
|
|
19
|
+
return "ssh"
|
|
20
|
+
}
|
|
21
|
+
return "ssh"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function sshCmdArgs(...args) {
|
|
25
|
+
const defaultOptions = [
|
|
26
|
+
`-o "StrictHostKeyChecking=no"`,
|
|
27
|
+
`-o "UserKnownHostsFile=/dev/null"`,
|
|
28
|
+
"-q",
|
|
29
|
+
"-p 22222",
|
|
30
|
+
`${isTraceMode() ? "-v" : ""}`
|
|
31
|
+
]
|
|
32
|
+
return [...defaultOptions, ...args]
|
|
33
|
+
}
|
|
7
34
|
|
|
8
35
|
export class DebugBridge {
|
|
9
36
|
constructor() {
|
|
10
37
|
this.uid = shellApi.uid
|
|
11
38
|
this.boxname = shellApi.boxname
|
|
12
39
|
this.domain = `dev.${this.boxname}.heiyu.space`
|
|
13
|
-
this.sshCmd = sshCmd(
|
|
14
|
-
`${isDebugMode() ? "-vv" : ""}`,
|
|
15
|
-
"-p 22222",
|
|
16
|
-
`box@${this.domain}`
|
|
17
|
-
)
|
|
18
40
|
}
|
|
19
41
|
|
|
20
42
|
async common(cmd, args) {
|
|
21
43
|
const resolvedIp = await resolveDomain(this.domain)
|
|
22
44
|
const resolvedCmd = cmd.replace(this.domain, resolvedIp)
|
|
23
|
-
const ssh =
|
|
45
|
+
const ssh = spawn.sync(resolvedCmd, args, {
|
|
24
46
|
shell: true,
|
|
25
47
|
encoding: "utf-8",
|
|
26
48
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -43,8 +65,11 @@ export class DebugBridge {
|
|
|
43
65
|
const stream = fs.createReadStream(lpkPath)
|
|
44
66
|
const resolvedIp = await resolveDomain(this.domain)
|
|
45
67
|
const ssh = spawn(
|
|
46
|
-
|
|
47
|
-
[
|
|
68
|
+
sshBinary(),
|
|
69
|
+
[
|
|
70
|
+
...sshCmdArgs(`box@${resolvedIp}`),
|
|
71
|
+
`install --uid ${this.uid} --pkgId ${pkgId}`
|
|
72
|
+
],
|
|
48
73
|
{
|
|
49
74
|
shell: true,
|
|
50
75
|
stdio: ["pipe", "inherit", "inherit"]
|
|
@@ -64,25 +89,44 @@ export class DebugBridge {
|
|
|
64
89
|
}
|
|
65
90
|
|
|
66
91
|
async sshCopyId() {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (!existed) {
|
|
70
|
-
logger.error("请检查 ssh-copy-id 是否安装,路径是否正确!")
|
|
71
|
-
process.exit(1)
|
|
72
|
-
}
|
|
92
|
+
const sshInfo = await findSshPublicKey()
|
|
93
|
+
logger.debug("ssh public key info", sshInfo)
|
|
73
94
|
|
|
74
95
|
const resolvedIp = await resolveDomain(this.domain)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
96
|
+
logger.debug("ip", resolvedIp)
|
|
97
|
+
|
|
98
|
+
// 将当前机器上的ssh public key 写到开发者工具中去,并过滤到重复的,管道在执行前会被清空,需要使用一个中间文件
|
|
99
|
+
const stream = spawn.sync(
|
|
100
|
+
sshBinary(),
|
|
101
|
+
[...sshCmdArgs(`box@${resolvedIp}`), `add-ssh-public-key`],
|
|
102
|
+
{
|
|
103
|
+
input: sshInfo.content.trimLeft(),
|
|
104
|
+
shell: true,
|
|
105
|
+
encoding: "utf-8",
|
|
106
|
+
stdio: "pipe"
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
if (stream.status != 0) {
|
|
110
|
+
const stderr = stream.stderr.toString("utf-8")
|
|
111
|
+
const stdout = stream.stdout.toString("utf-8")
|
|
112
|
+
throw stdout + stderr
|
|
113
|
+
}
|
|
114
|
+
if (stream.error) {
|
|
115
|
+
throw stream.error
|
|
116
|
+
}
|
|
78
117
|
}
|
|
79
118
|
|
|
80
119
|
async status(appId) {
|
|
81
|
-
return this.common(
|
|
120
|
+
return this.common(sshBinary(), [
|
|
121
|
+
...sshCmdArgs(`box@${this.domain}`),
|
|
122
|
+
`status --uid ${this.uid}`,
|
|
123
|
+
appId
|
|
124
|
+
])
|
|
82
125
|
}
|
|
83
126
|
|
|
84
127
|
async isDevshell(appId) {
|
|
85
|
-
const stdout = await this.common(
|
|
128
|
+
const stdout = await this.common(sshBinary(), [
|
|
129
|
+
...sshCmdArgs(`box@${this.domain}`),
|
|
86
130
|
`isDevshell --uid ${this.uid}`,
|
|
87
131
|
appId
|
|
88
132
|
])
|
|
@@ -90,11 +134,19 @@ export class DebugBridge {
|
|
|
90
134
|
}
|
|
91
135
|
|
|
92
136
|
async resume(appId) {
|
|
93
|
-
return this.common(
|
|
137
|
+
return this.common(sshBinary(), [
|
|
138
|
+
...sshCmdArgs(`box@${this.domain}`),
|
|
139
|
+
`resume --uid ${this.uid}`,
|
|
140
|
+
appId
|
|
141
|
+
])
|
|
94
142
|
}
|
|
95
143
|
|
|
96
144
|
async uninstall(appId) {
|
|
97
|
-
return this.common(
|
|
145
|
+
return this.common(sshBinary(), [
|
|
146
|
+
...sshCmdArgs(`box@${this.domain}`),
|
|
147
|
+
`uninstall --uid ${this.uid}`,
|
|
148
|
+
appId
|
|
149
|
+
])
|
|
98
150
|
}
|
|
99
151
|
|
|
100
152
|
async devshell(appId, isUserApp, onconnect = null) {
|
|
@@ -111,13 +163,15 @@ export class DebugBridge {
|
|
|
111
163
|
const resolvedIp = await resolveDomain(this.domain)
|
|
112
164
|
|
|
113
165
|
const stream = spawn(
|
|
114
|
-
|
|
166
|
+
sshBinary(),
|
|
115
167
|
[
|
|
168
|
+
...sshCmdArgs(`box@${resolvedIp}`),
|
|
116
169
|
"-t",
|
|
117
170
|
"devshell",
|
|
118
171
|
`--uid ${this.uid}`,
|
|
119
172
|
isUserApp ? "--userapp" : "",
|
|
120
173
|
appId,
|
|
174
|
+
"/bin/sh",
|
|
121
175
|
"/lzcapp/pkg/content/devshell/exec.sh"
|
|
122
176
|
],
|
|
123
177
|
{
|
|
@@ -142,17 +196,17 @@ export class DebugBridge {
|
|
|
142
196
|
const resolvedIp = await resolveDomain(this.domain)
|
|
143
197
|
const stream = fs.createReadStream(contextTar)
|
|
144
198
|
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
[`build --tag ${tag}`],
|
|
199
|
+
const buildStream = spawn(
|
|
200
|
+
sshBinary(),
|
|
201
|
+
[...sshCmdArgs(`box@${resolvedIp}`), `build --tag ${tag}`],
|
|
148
202
|
{
|
|
149
203
|
shell: true,
|
|
150
204
|
stdio: ["pipe", "inherit", "inherit"]
|
|
151
205
|
}
|
|
152
206
|
)
|
|
153
|
-
stream.pipe(
|
|
207
|
+
stream.pipe(buildStream.stdin)
|
|
154
208
|
return new Promise((resolve, reject) => {
|
|
155
|
-
|
|
209
|
+
buildStream.on("close", (code) => {
|
|
156
210
|
code == 0
|
|
157
211
|
? resolve(`dev.${this.boxname}.heiyu.space/${tag}`)
|
|
158
212
|
: reject("在盒子中构建 image 失败")
|
package/lib/app/lpk_devshell.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import path from "node:path"
|
|
3
3
|
import fs from "node:fs"
|
|
4
4
|
import logger from "loglevel"
|
|
5
|
-
import
|
|
5
|
+
import spawn from "cross-spawn"
|
|
6
6
|
import { LpkInstaller } from "./lpk_installer.js"
|
|
7
7
|
import debounce from "lodash.debounce"
|
|
8
8
|
import {
|
|
@@ -18,7 +18,10 @@ import {
|
|
|
18
18
|
isUserApp,
|
|
19
19
|
createTemplateFileCommon,
|
|
20
20
|
isDebugMode,
|
|
21
|
-
resolveDomain
|
|
21
|
+
resolveDomain,
|
|
22
|
+
isWindows,
|
|
23
|
+
isMacOs,
|
|
24
|
+
isLinux
|
|
22
25
|
} from "../utils.js"
|
|
23
26
|
import os from "node:os"
|
|
24
27
|
import commandExists from "command-exists"
|
|
@@ -152,36 +155,60 @@ export class AppDevShell {
|
|
|
152
155
|
this.lpkBuild.onBeforeTarContent(async (contentdir) => {
|
|
153
156
|
const busyboxPath = path.join(
|
|
154
157
|
contextDirname(import.meta.url),
|
|
155
|
-
"
|
|
158
|
+
"..",
|
|
159
|
+
"..",
|
|
160
|
+
"template",
|
|
161
|
+
"_lpk",
|
|
162
|
+
"busybox-1.35.0"
|
|
156
163
|
)
|
|
157
164
|
let dest = path.join(contentdir, "devshell", "busybox")
|
|
158
165
|
ensureDir(dest)
|
|
159
166
|
fs.copyFileSync(busyboxPath, dest)
|
|
160
|
-
fs.chmodSync(dest, 0o775)
|
|
161
167
|
})
|
|
162
168
|
|
|
163
169
|
// 复制 init_debug_bridge.sh 到 devshell 中去
|
|
164
170
|
this.lpkBuild.onBeforeTarContent(async (contentdir) => {
|
|
165
171
|
const initPath = path.join(
|
|
166
172
|
contextDirname(import.meta.url),
|
|
167
|
-
"
|
|
173
|
+
"..",
|
|
174
|
+
"..",
|
|
175
|
+
"template",
|
|
176
|
+
"_lpk",
|
|
177
|
+
"init_debug_bridge.sh"
|
|
168
178
|
)
|
|
169
179
|
let dest = path.join(contentdir, "devshell", "init_debug_bridge.sh")
|
|
170
180
|
ensureDir(dest)
|
|
171
181
|
fs.copyFileSync(initPath, dest)
|
|
172
|
-
fs.chmodSync(dest, 0o775)
|
|
173
182
|
})
|
|
174
183
|
|
|
175
184
|
// 复制 exec.sh 到 devshell 中去
|
|
176
185
|
this.lpkBuild.onBeforeTarContent(async (contentdir) => {
|
|
177
186
|
const execScriptPath = path.join(
|
|
178
187
|
contextDirname(import.meta.url),
|
|
179
|
-
"
|
|
188
|
+
"..",
|
|
189
|
+
"..",
|
|
190
|
+
"template",
|
|
191
|
+
"_lpk",
|
|
192
|
+
"exec.sh"
|
|
180
193
|
)
|
|
181
194
|
let dest = path.join(contentdir, "devshell", "exec.sh")
|
|
182
195
|
ensureDir(dest)
|
|
183
196
|
fs.copyFileSync(execScriptPath, dest)
|
|
184
|
-
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
// 复制 rsyncd.conf 到 devshell 中去
|
|
200
|
+
this.lpkBuild.onBeforeTarContent(async (contentdir) => {
|
|
201
|
+
const execScriptPath = path.join(
|
|
202
|
+
contextDirname(import.meta.url),
|
|
203
|
+
"..",
|
|
204
|
+
"..",
|
|
205
|
+
"template",
|
|
206
|
+
"_lpk",
|
|
207
|
+
"rsyncd.conf"
|
|
208
|
+
)
|
|
209
|
+
let dest = path.join(contentdir, "devshell", "rsyncd.conf")
|
|
210
|
+
ensureDir(dest)
|
|
211
|
+
fs.copyFileSync(execScriptPath, dest)
|
|
185
212
|
})
|
|
186
213
|
|
|
187
214
|
// 复制 setupscript 脚本
|
|
@@ -202,7 +229,6 @@ export class AppDevShell {
|
|
|
202
229
|
} else {
|
|
203
230
|
fs.writeFileSync(dest, `#!/bin/sh\nset -ex\n${devshell["setupscript"]}`)
|
|
204
231
|
}
|
|
205
|
-
fs.chmodSync(dest, 0o775)
|
|
206
232
|
})
|
|
207
233
|
|
|
208
234
|
// 在生成 manifest.yml 之前合并 devshell manifest 模板字段
|
|
@@ -214,9 +240,10 @@ export class AppDevShell {
|
|
|
214
240
|
logger.debug("options devshell delete 'routes' field")
|
|
215
241
|
delete options["devshell"]["routes"]
|
|
216
242
|
|
|
217
|
-
// 添加 devshell
|
|
243
|
+
// 添加 devshell 必要路由,这里需要使用 /bin/sh 启动后面的脚本,因为在
|
|
244
|
+
// Windows 上打包的文件将会丢失可执行权限
|
|
218
245
|
routes.push(
|
|
219
|
-
"/__debug.bridge=exec://80,/lzcapp/pkg/content/devshell/init_debug_bridge.sh"
|
|
246
|
+
"/__debug.bridge=exec://80,/bin/sh /lzcapp/pkg/content/devshell/init_debug_bridge.sh"
|
|
220
247
|
)
|
|
221
248
|
routes.push("/__isdevshell=file:///lzcapp/pkg/devshell")
|
|
222
249
|
|
|
@@ -281,7 +308,11 @@ export class AppDevShell {
|
|
|
281
308
|
try {
|
|
282
309
|
const dockerfilePath = path.join(
|
|
283
310
|
contextDirname(import.meta.url),
|
|
284
|
-
"
|
|
311
|
+
"..",
|
|
312
|
+
"..",
|
|
313
|
+
"template",
|
|
314
|
+
"_lpk",
|
|
315
|
+
"Dockerfile.in"
|
|
285
316
|
)
|
|
286
317
|
await createTemplateFileCommon(
|
|
287
318
|
dockerfilePath,
|
|
@@ -413,42 +444,52 @@ class DevShell {
|
|
|
413
444
|
}
|
|
414
445
|
|
|
415
446
|
async syncProject(appId) {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
`22222`,
|
|
424
|
-
"-o",
|
|
425
|
-
`"StrictHostKeyChecking=no"`,
|
|
426
|
-
"-o",
|
|
427
|
-
`"UserKnownHostsFile=/dev/null"`,
|
|
428
|
-
"-o",
|
|
429
|
-
`"ConnectionAttempts=3"`,
|
|
430
|
-
"-o",
|
|
431
|
-
`"ConnectTimeout=30"`,
|
|
432
|
-
"-o",
|
|
433
|
-
`${isDebugMode() ? '"LogLevel=DEBUG"' : '"LogLevel=ERROR"'}`
|
|
434
|
-
].join(" ")
|
|
435
|
-
// 检查rsync工具是否存在:提示用户
|
|
436
|
-
const rsyncExisted = commandExists.sync("rsync")
|
|
437
|
-
if (!rsyncExisted) {
|
|
438
|
-
logger.error("请检查 rsync 是否安装,路径是否正确!")
|
|
439
|
-
process.exit(1)
|
|
447
|
+
if (isLinux || isMacOs) {
|
|
448
|
+
// 检查rsync工具是否存在:提示用户
|
|
449
|
+
const rsyncExisted = commandExists.sync("rsync")
|
|
450
|
+
if (!rsyncExisted) {
|
|
451
|
+
logger.error("请检查 rsync 是否安装,路径是否正确!")
|
|
452
|
+
process.exit(1)
|
|
453
|
+
}
|
|
440
454
|
}
|
|
441
455
|
|
|
456
|
+
const resolvedIp = await resolveDomain(
|
|
457
|
+
`dev.${shellApi.boxname}.heiyu.space`
|
|
458
|
+
)
|
|
442
459
|
const rsyncDebug = isDebugMode() ? "-P" : ""
|
|
443
460
|
const host = this.isUserApp
|
|
444
461
|
? `${shellApi.uid}.app.${appId}.lzcapp`
|
|
445
462
|
: `app.${appId}.lzcapp`
|
|
446
|
-
const dest = `
|
|
463
|
+
const dest = `rsync://${host}@[${resolvedIp}]:873/lzcapp/cache/devshell`
|
|
464
|
+
const rsyncCmd = isWindows
|
|
465
|
+
? path.join(
|
|
466
|
+
contextDirname(import.meta.url),
|
|
467
|
+
"..",
|
|
468
|
+
"..",
|
|
469
|
+
"template",
|
|
470
|
+
"_lpk",
|
|
471
|
+
"win-rsync",
|
|
472
|
+
"rsync.exe"
|
|
473
|
+
)
|
|
474
|
+
: `rsync`
|
|
475
|
+
|
|
447
476
|
try {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
477
|
+
const rsyncArgs = [
|
|
478
|
+
`${rsyncDebug}`,
|
|
479
|
+
`--recursive`,
|
|
480
|
+
`--relative`,
|
|
481
|
+
`--perms`,
|
|
482
|
+
`--update`,
|
|
483
|
+
`-F --filter=':- .gitignore'`,
|
|
484
|
+
`--ignore-errors`,
|
|
485
|
+
`. ${dest}`
|
|
486
|
+
]
|
|
487
|
+
logger.debug("同步代码: ", rsyncCmd, rsyncArgs.join(" "))
|
|
488
|
+
const rsyncStream = spawn.sync(rsyncCmd, rsyncArgs, {
|
|
489
|
+
env: { RSYNC_PASSWORD: "fakefakefake" },
|
|
490
|
+
shell: true,
|
|
491
|
+
stdio: ["ignore", isDebugMode() ? "inherit" : "ignore", "inherit"]
|
|
492
|
+
})
|
|
452
493
|
} catch (err) {
|
|
453
494
|
logger.error("rsync 同步失败")
|
|
454
495
|
logger.debug(err)
|
package/lib/shellapi.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import grpc from "@grpc/grpc-js"
|
|
2
2
|
import protoLoader from "@grpc/proto-loader"
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
contextDirname,
|
|
5
|
+
isMacOs,
|
|
6
|
+
isWindows,
|
|
7
|
+
isLinux,
|
|
8
|
+
isFileExist
|
|
9
|
+
} from "./utils.js"
|
|
4
10
|
import path from "node:path"
|
|
5
11
|
import fs from "node:fs"
|
|
6
12
|
import os from "node:os"
|
|
@@ -15,9 +21,9 @@ const bannerfileContent = `˄=ᆽ=ᐟ \\`
|
|
|
15
21
|
function getShellAPIConfigDir() {
|
|
16
22
|
const home = os.homedir()
|
|
17
23
|
let suffix = "/.config/hportal-client"
|
|
18
|
-
if (
|
|
24
|
+
if (isMacOs) {
|
|
19
25
|
suffix = "/Library/Application Support/hportal-client"
|
|
20
|
-
} else if (
|
|
26
|
+
} else if (isWindows) {
|
|
21
27
|
suffix = "\\AppData\\Roaming\\hportal-client"
|
|
22
28
|
}
|
|
23
29
|
const SHELLAPI_CONFIG_DIR = path.join(home, suffix)
|
|
@@ -28,19 +34,49 @@ class ShellApi {
|
|
|
28
34
|
constructor() {}
|
|
29
35
|
|
|
30
36
|
async init() {
|
|
31
|
-
const pbShell = this.initShell()
|
|
32
|
-
const { addr, cred } = this.readShellApiInfo()
|
|
33
|
-
this.client = new pbShell.ShellCore(addr, grpc.credentials.createInsecure())
|
|
34
|
-
|
|
35
37
|
// 检查当前 shell 环境上下文是否配置 HTTP_PROXY
|
|
36
38
|
if (this.checkEnvProxy()) {
|
|
37
|
-
|
|
39
|
+
logger.warn(
|
|
40
|
+
`WARN:: 当前终端环境已配置 HTTP_PROXY 代理,可能会导致访问懒猫微服失败,如果未影响功能可以忽略该警告`
|
|
41
|
+
)
|
|
38
42
|
}
|
|
39
43
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
const SHELLAPI_CONFIG_DIR = getShellAPIConfigDir()
|
|
45
|
+
const addrFile = path.resolve(SHELLAPI_CONFIG_DIR, "shellapi_addr")
|
|
46
|
+
const credFile = path.resolve(SHELLAPI_CONFIG_DIR, "shellapi_cred")
|
|
47
|
+
if (!isFileExist(addrFile) || !isFileExist(credFile)) {
|
|
48
|
+
logger.warn(`WARN:: 读取不到以下路径中的文件,无法连接客户端自动判读盒子名称和盒子用户,
|
|
49
|
+
|
|
50
|
+
${addrFile} 盒子的连接地址
|
|
51
|
+
${credFile} 盒子的连接凭证
|
|
52
|
+
|
|
53
|
+
但可使用环境变量指定:
|
|
54
|
+
- BOX_NAME=foo 指定盒子的名称
|
|
55
|
+
- BOX_UID=bar 指定盒子的用户
|
|
56
|
+
|
|
57
|
+
NOTE:在指定环境变量的模式下,有些接口依旧不能访问,为了更好的开发体验,建议尽量在同主机的环境下启动客户端`)
|
|
58
|
+
const md = new grpc.Metadata()
|
|
59
|
+
md.add("lzc-shellapi-cred", "fakecred")
|
|
60
|
+
this.metadata = md
|
|
61
|
+
const boxUid = process.env.BOX_UID
|
|
62
|
+
const boxName = process.env.BOX_NAME
|
|
63
|
+
if (!boxUid || !boxName) {
|
|
64
|
+
process.exit(1)
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
this.info = { uid: boxUid, boxname: boxName }
|
|
68
|
+
} else {
|
|
69
|
+
const pbShell = this.initShell()
|
|
70
|
+
const { addr, cred } = this.readShellApiInfo()
|
|
71
|
+
this.client = new pbShell.ShellCore(
|
|
72
|
+
addr,
|
|
73
|
+
grpc.credentials.createInsecure()
|
|
74
|
+
)
|
|
75
|
+
const md = new grpc.Metadata()
|
|
76
|
+
md.add("lzc-shellapi-cred", cred)
|
|
77
|
+
this.metadata = md
|
|
78
|
+
this.info = await this.initBoxInfo()
|
|
79
|
+
}
|
|
44
80
|
this.checkDevTools()
|
|
45
81
|
}
|
|
46
82
|
|
|
@@ -121,14 +157,15 @@ class ShellApi {
|
|
|
121
157
|
}
|
|
122
158
|
|
|
123
159
|
checkEnvProxy() {
|
|
124
|
-
if(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
160
|
+
if (
|
|
161
|
+
process.env.HTTPS_PROXY ||
|
|
162
|
+
process.env.HTTP_PROXY ||
|
|
163
|
+
process.env.ALL_PROXY ||
|
|
164
|
+
process.env.https_proxy ||
|
|
165
|
+
process.env.http_proxy ||
|
|
166
|
+
process.env.all_proxy
|
|
167
|
+
) {
|
|
168
|
+
return true
|
|
132
169
|
}
|
|
133
170
|
return false
|
|
134
171
|
}
|
package/lib/utils.js
CHANGED
|
@@ -12,7 +12,7 @@ import https from "node:https"
|
|
|
12
12
|
import http from "node:http"
|
|
13
13
|
import zlib from "node:zlib"
|
|
14
14
|
import process from "node:process"
|
|
15
|
-
import
|
|
15
|
+
import spawn from "cross-spawn"
|
|
16
16
|
import logger from "loglevel"
|
|
17
17
|
import * as tar from "tar"
|
|
18
18
|
import dns from "node:dns/promises"
|
|
@@ -33,6 +33,10 @@ api.interceptors.response.use(
|
|
|
33
33
|
}
|
|
34
34
|
)
|
|
35
35
|
|
|
36
|
+
export const isMacOs = process.platform == "darwin"
|
|
37
|
+
export const isLinux = process.platform == "linux"
|
|
38
|
+
export const isWindows = process.platform == "win32"
|
|
39
|
+
|
|
36
40
|
export const envsubstr = async (templateContents, args) => {
|
|
37
41
|
const parse = await importDefault("envsub/js/envsub-parser.js")
|
|
38
42
|
return parse(templateContents, args)
|
|
@@ -350,7 +354,7 @@ export class FileLocker {
|
|
|
350
354
|
}
|
|
351
355
|
}
|
|
352
356
|
withOpen(filename) {
|
|
353
|
-
const p =
|
|
357
|
+
const p = spawn.sync("lsof", ["-w", "-t", filename])
|
|
354
358
|
if (p.status === 0) {
|
|
355
359
|
return true
|
|
356
360
|
} else {
|
|
@@ -438,6 +442,10 @@ export function isDebugMode() {
|
|
|
438
442
|
return logger.getLevel() <= logger.levels.DEBUG
|
|
439
443
|
}
|
|
440
444
|
|
|
445
|
+
export function isTraceMode() {
|
|
446
|
+
return logger.getLevel() <= logger.levels.TRACE
|
|
447
|
+
}
|
|
448
|
+
|
|
441
449
|
export async function resolveDomain(domain) {
|
|
442
450
|
try {
|
|
443
451
|
// Set machine's dns server as defalut dns server
|
|
@@ -466,17 +474,6 @@ export async function resolveDomain(domain) {
|
|
|
466
474
|
}
|
|
467
475
|
}
|
|
468
476
|
|
|
469
|
-
export function sshCmd(...args) {
|
|
470
|
-
const baseCommand = "ssh"
|
|
471
|
-
const defaultOptions = [
|
|
472
|
-
"-o StrictHostKeyChecking=no",
|
|
473
|
-
"-o UserKnownHostsFile=/dev/null",
|
|
474
|
-
"-q"
|
|
475
|
-
]
|
|
476
|
-
const options = [...defaultOptions, ...args].join(" ")
|
|
477
|
-
return `${baseCommand} ${options}`
|
|
478
|
-
}
|
|
479
|
-
|
|
480
477
|
export function unzipSync(zipPath, destPath) {
|
|
481
478
|
// 确保目标目录存在
|
|
482
479
|
fs.mkdirSync(destPath, { recursive: true })
|
|
@@ -486,3 +483,94 @@ export function unzipSync(zipPath, destPath) {
|
|
|
486
483
|
// 同步解压所有文件
|
|
487
484
|
zip.extractAllTo(destPath, true)
|
|
488
485
|
}
|
|
486
|
+
|
|
487
|
+
export function findSshPublicKey() {
|
|
488
|
+
// 获取用户 HOME 目录
|
|
489
|
+
let homeDir
|
|
490
|
+
switch (process.platform) {
|
|
491
|
+
case "win32":
|
|
492
|
+
// Windows: 通常是 C:\Users\username
|
|
493
|
+
homeDir = process.env.USERPROFILE || os.homedir()
|
|
494
|
+
break
|
|
495
|
+
case "darwin":
|
|
496
|
+
case "linux":
|
|
497
|
+
// macOS 和 Linux: 通常是 /Users/username 或 /home/username
|
|
498
|
+
homeDir = process.env.HOME || os.homedir()
|
|
499
|
+
break
|
|
500
|
+
default:
|
|
501
|
+
throw new Error("Unsupported operating system")
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const sshDir = path.join(homeDir, ".ssh")
|
|
505
|
+
|
|
506
|
+
// 检查 .ssh 目录是否存在
|
|
507
|
+
try {
|
|
508
|
+
const dirStat = fs.statSync(sshDir)
|
|
509
|
+
if (!dirStat.isDirectory()) {
|
|
510
|
+
throw new Error(
|
|
511
|
+
".ssh 目前存在,但不是一个正常的目录,请确保 ssh 已经安装,以及 ssh-keygen 生成公钥"
|
|
512
|
+
)
|
|
513
|
+
}
|
|
514
|
+
} catch (error) {
|
|
515
|
+
if (error.code === "ENOENT") {
|
|
516
|
+
throw new Error(
|
|
517
|
+
".ssh 目录不存在, 请确保 ssh 已经安装,以及 ssh-keygen 生成公钥"
|
|
518
|
+
)
|
|
519
|
+
}
|
|
520
|
+
throw error
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// 定义公钥优先级列表
|
|
524
|
+
const priorityKeys = [
|
|
525
|
+
"id_ed25519.pub",
|
|
526
|
+
"id_rsa.pub",
|
|
527
|
+
"id_ecdsa.pub",
|
|
528
|
+
"id_dsa.pub"
|
|
529
|
+
]
|
|
530
|
+
|
|
531
|
+
try {
|
|
532
|
+
// 获取 .ssh 目录下所有文件
|
|
533
|
+
const files = fs.readdirSync(sshDir)
|
|
534
|
+
|
|
535
|
+
// 首先检查优先级列表中的公钥
|
|
536
|
+
for (const keyName of priorityKeys) {
|
|
537
|
+
if (files.includes(keyName)) {
|
|
538
|
+
const keyPath = path.join(sshDir, keyName)
|
|
539
|
+
// 验证文件是否可读
|
|
540
|
+
try {
|
|
541
|
+
fs.accessSync(keyPath, fs.constants.R_OK)
|
|
542
|
+
return {
|
|
543
|
+
path: keyPath,
|
|
544
|
+
content: fs.readFileSync(keyPath, "utf8")
|
|
545
|
+
}
|
|
546
|
+
} catch (error) {
|
|
547
|
+
console.warn(`Found ${keyName} but cannot read it:`, error.message)
|
|
548
|
+
continue
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// 如果没找到优先级列表中的公钥,查找任何 .pub 结尾的文件
|
|
554
|
+
const pubKey = files.find((file) => file.endsWith(".pub"))
|
|
555
|
+
if (pubKey) {
|
|
556
|
+
const keyPath = path.join(sshDir, pubKey)
|
|
557
|
+
try {
|
|
558
|
+
fs.accessSync(keyPath, fs.constants.R_OK)
|
|
559
|
+
return {
|
|
560
|
+
path: keyPath,
|
|
561
|
+
content: fs.readFileSync(keyPath, "utf8")
|
|
562
|
+
}
|
|
563
|
+
} catch (error) {
|
|
564
|
+
throw new Error(`Found ${pubKey} but cannot read it: ${error.message}`)
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
throw new Error(
|
|
568
|
+
".ssh 目录没有没有找到任何 .pub 公钥. 请使用 ssh-keygen 生成"
|
|
569
|
+
)
|
|
570
|
+
} catch (error) {
|
|
571
|
+
if (error.code === "ENOENT") {
|
|
572
|
+
throw new Error("不能查询 .ssh 目录")
|
|
573
|
+
}
|
|
574
|
+
throw error
|
|
575
|
+
}
|
|
576
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazycatcloud/lzc-cli",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.41",
|
|
4
4
|
"description": "lazycat cloud developer kit",
|
|
5
5
|
"files": [
|
|
6
6
|
"template",
|
|
@@ -20,12 +20,14 @@
|
|
|
20
20
|
"@balena/dockerignore": "^1.0.2",
|
|
21
21
|
"@grpc/grpc-js": "^1.11.1",
|
|
22
22
|
"@grpc/proto-loader": "^0.7.13",
|
|
23
|
+
"@lazycatcloud/sdk": "^0.1.423",
|
|
23
24
|
"adm-zip": "^0.5.16",
|
|
24
25
|
"archiver": "^7.0.1",
|
|
25
26
|
"axios": "^1.7.7",
|
|
26
27
|
"chalk": "^5.3.0",
|
|
27
28
|
"chokidar": "^3.6.0",
|
|
28
29
|
"command-exists": "^1.2.9",
|
|
30
|
+
"cross-spawn": "^7.0.3",
|
|
29
31
|
"dockerfile-ast": "^0.6.1",
|
|
30
32
|
"envsub": "^4.1.0",
|
|
31
33
|
"fast-glob": "^3.3.2",
|
|
File without changes
|
package/template/_lpk/exec.sh
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# 从1号进程读取环境变量
|
|
5
5
|
cat > /tmp/lzcapp_env.sh << EOF
|
|
6
6
|
#!/bin/sh
|
|
7
|
-
$(/
|
|
7
|
+
$(/usr/lib/debug.bridge/busybox tr '\0' '\n' < /proc/1/environ | /usr/lib/debug.bridge/busybox xargs -I{} echo export {})
|
|
8
8
|
EOF
|
|
9
9
|
. /tmp/lzcapp_env.sh
|
|
10
10
|
|
|
@@ -2,30 +2,38 @@
|
|
|
2
2
|
set -e
|
|
3
3
|
|
|
4
4
|
DEBUG_BRIDGE_LZCAPP=http://app.cloud.lazycat.developer.tools.lzcapp
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
mkdir -p /usr/lib/debug.bridge/
|
|
7
|
+
# 不能直接对 pkg content 中的 busybox chmod(Read-Only权限)
|
|
8
|
+
LZCBUSYBOX=/usr/lib/debug.bridge/busybox
|
|
9
|
+
cp /lzcapp/pkg/content/devshell/busybox ${LZCBUSYBOX}
|
|
10
|
+
chmod +x ${LZCBUSYBOX}
|
|
6
11
|
|
|
7
12
|
mkdir -p /root/.ssh
|
|
8
13
|
mkdir -p /etc/debug.bridge
|
|
9
14
|
|
|
10
15
|
if ! [ -f /usr/bin/dropbearmulti ]; then
|
|
11
|
-
${
|
|
16
|
+
${LZCBUSYBOX} wget ${DEBUG_BRIDGE_LZCAPP}/resources/dropbearmulti -O /usr/bin/dropbearmulti
|
|
12
17
|
chmod +x /usr/bin/dropbearmulti
|
|
13
18
|
fi
|
|
14
19
|
|
|
15
20
|
if ! [ -f /usr/bin/rsync ]; then
|
|
16
|
-
${
|
|
21
|
+
${LZCBUSYBOX} wget ${DEBUG_BRIDGE_LZCAPP}/resources/rsync -O /usr/bin/rsync
|
|
17
22
|
chmod +x /usr/bin/rsync
|
|
18
23
|
fi
|
|
19
24
|
|
|
20
|
-
${
|
|
25
|
+
${LZCBUSYBOX} wget ${DEBUG_BRIDGE_LZCAPP}/resources/authorized_keys -O /root/.ssh/authorized_keys
|
|
21
26
|
chmod 0644 /root/.ssh/authorized_keys
|
|
22
27
|
|
|
23
|
-
${
|
|
28
|
+
${LZCBUSYBOX} wget ${DEBUG_BRIDGE_LZCAPP}/bannerfile -O /etc/debug.bridge/bannerfile
|
|
24
29
|
|
|
25
|
-
if ${
|
|
30
|
+
if ${LZCBUSYBOX} netstat -tln | grep ':22222' >/dev/null; then
|
|
26
31
|
echo "端口22222正在监听"
|
|
27
|
-
${
|
|
32
|
+
${LZCBUSYBOX} sleep infinity
|
|
28
33
|
else
|
|
34
|
+
echo "启动 rsync daemon."
|
|
35
|
+
rsync --daemon --no-detach --config=/lzcapp/pkg/content/devshell/rsyncd.conf &
|
|
36
|
+
|
|
29
37
|
mkdir -p /etc/dropbear
|
|
30
38
|
exec dropbearmulti dropbear -R -F -E -B -p 22222
|
|
31
39
|
fi
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# 全局参数
|
|
2
|
+
uid = root
|
|
3
|
+
gid = root
|
|
4
|
+
use chroot = no
|
|
5
|
+
max connections = 4
|
|
6
|
+
timeout = 300
|
|
7
|
+
pid file = /var/run/rsyncd.pid
|
|
8
|
+
lock file = /var/run/rsync.lock
|
|
9
|
+
|
|
10
|
+
# 实时输出设置
|
|
11
|
+
log file = /dev/stdout
|
|
12
|
+
transfer logging = yes
|
|
13
|
+
log format = %t rsync [%p] %o %h [%a] %m %f %l
|
|
14
|
+
|
|
15
|
+
[lzcapp]
|
|
16
|
+
path = /lzcapp
|
|
17
|
+
read only = no
|
|
18
|
+
dont compress = *.gz *.bz2 *.zip
|
|
19
|
+
|
|
20
|
+
[root]
|
|
21
|
+
path = /root/
|
|
22
|
+
read only = no
|
|
23
|
+
dont compress = *.gz *.bz2 *.zip
|
|
24
|
+
|
|
25
|
+
[tmp]
|
|
26
|
+
path = /tmp/
|
|
27
|
+
read only = no
|
|
28
|
+
dont compress = *.gz *.bz2 *.zip
|
|
Binary file
|
|
Binary file
|