k3s-deployer 1.0.0 → 1.0.1
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/package.json
CHANGED
|
@@ -1,154 +1,57 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const path = require('node:path');
|
|
4
|
-
const crypto = require('node:crypto');
|
|
5
4
|
const { upsertCloudflareDnsRecord } = require('../dns/cloudflare.cjs');
|
|
6
5
|
|
|
7
6
|
/* ────────────────────────────────────────────────────────────────────────────
|
|
8
|
-
* redroid + scrcpy
|
|
7
|
+
* redroid + ws-scrcpy adapter
|
|
9
8
|
*
|
|
10
9
|
* Runs Android natively on ARM64 via redroid (no emulation).
|
|
11
|
-
* Uses
|
|
10
|
+
* Uses ws-scrcpy sidecar for browser-based display (H.264 WebSocket).
|
|
12
11
|
*
|
|
13
12
|
* Flow:
|
|
14
13
|
* 1. Load binder kernel module (init container)
|
|
15
14
|
* 2. Start redroid container (native ARM64 Android)
|
|
16
|
-
* 3. Start
|
|
15
|
+
* 3. Start ws-scrcpy sidecar (direct H.264 streaming)
|
|
17
16
|
* 4. Install APK via adb
|
|
18
|
-
* 5. Browser preview via
|
|
17
|
+
* 5. Browser preview via ws-scrcpy web UI
|
|
19
18
|
* ──────────────────────────────────────────────────────────────────────────── */
|
|
20
19
|
|
|
21
20
|
const REDROID_IMAGE = 'docker.io/redroid/redroid:12.0.0-latest';
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
22
|
+
/* ws-scrcpy uses a pre-built image from the local registry */
|
|
23
|
+
const WS_SCRCPY_IMAGE = 'ws-scrcpy:latest';
|
|
24
|
+
|
|
25
|
+
const WS_SCRCPY_PORT = 8000;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generate a custom index.html for ws-scrcpy that auto-redirects to the
|
|
29
|
+
* H.264 MSE stream and centers the video with high quality display.
|
|
30
|
+
*/
|
|
31
|
+
function generateStreamIndexHtml() {
|
|
32
|
+
return `<!doctype html><html lang="en"><head><meta charset="UTF-8">
|
|
33
|
+
<meta content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" name="viewport"/>
|
|
34
|
+
<title>Mobile Preview</title>
|
|
35
|
+
<script>
|
|
36
|
+
(function(){
|
|
37
|
+
var h="#!action=stream&udid=127.0.0.1%3A5555&player=webcodecs&ws=wss%3A%2F%2F"+location.host+"%2F%3Faction%3Dproxy-adb%26remote%3Dtcp%253A8886%26udid%3D127.0.0.1%253A5555";
|
|
38
|
+
if(!location.hash||location.hash==="#"||location.hash.indexOf("action=stream")===-1){
|
|
39
|
+
window.location.hash=h;window.location.reload();
|
|
40
|
+
}
|
|
41
|
+
})();
|
|
42
|
+
</script>
|
|
43
|
+
<script defer="defer" src="bundle.js"></script>
|
|
44
|
+
<link href="main.css" rel="stylesheet">
|
|
45
|
+
<style>
|
|
46
|
+
*{margin:0!important;padding:0!important;box-sizing:border-box!important}
|
|
47
|
+
html,body{width:100%!important;height:100%!important;background:#000!important;overflow:hidden!important}
|
|
48
|
+
body,.device-view,.stream{position:absolute!important;top:0!important;left:0!important;width:100%!important;height:100%!important;display:flex!important;justify-content:center!important;align-items:center!important}
|
|
49
|
+
.video,.video-layer{display:flex!important;justify-content:center!important;align-items:center!important;width:100%!important;height:100%!important}
|
|
50
|
+
video,canvas{display:block!important;height:100vh!important;width:auto!important;max-width:100vw!important;object-fit:contain!important}
|
|
51
|
+
.control-buttons,.toolbar{position:fixed!important;right:0!important;top:50%!important;transform:translateY(-50%)!important;z-index:999!important}
|
|
52
|
+
</style></head><body></body></html>
|
|
40
53
|
`;
|
|
41
|
-
|
|
42
|
-
const DISPLAY_ENTRYPOINT = `#!/usr/bin/env bash
|
|
43
|
-
set -euo pipefail
|
|
44
|
-
|
|
45
|
-
cleanup() {
|
|
46
|
-
pkill -f scrcpy || true
|
|
47
|
-
pkill -f x11vnc || true
|
|
48
|
-
pkill -f Xvfb || true
|
|
49
|
-
pkill -f novnc_proxy || true
|
|
50
54
|
}
|
|
51
|
-
trap cleanup SIGINT SIGTERM EXIT
|
|
52
|
-
|
|
53
|
-
export DISPLAY=:99
|
|
54
|
-
|
|
55
|
-
echo "[display] Starting Xvfb"
|
|
56
|
-
Xvfb :99 -screen 0 720x1280x24 -ac +extension GLX +render -noreset &
|
|
57
|
-
sleep 1
|
|
58
|
-
|
|
59
|
-
echo "[display] Starting x11vnc"
|
|
60
|
-
x11vnc -display :99 -forever -shared -nopw -rfbport 5900 -listen 0.0.0.0 -xkb &
|
|
61
|
-
|
|
62
|
-
echo "[display] Starting noVNC on port 6080"
|
|
63
|
-
/opt/noVNC/utils/novnc_proxy --vnc 127.0.0.1:5900 --listen 6080 &
|
|
64
|
-
sleep 1
|
|
65
|
-
|
|
66
|
-
echo "[display] Starting network routing fix loop"
|
|
67
|
-
(while true; do ip rule del from all unreachable 2>/dev/null || true; sleep 2; done) &
|
|
68
|
-
|
|
69
|
-
echo "[display] Waiting for Android (adb)..."
|
|
70
|
-
for i in \$(seq 1 180); do
|
|
71
|
-
if adb connect 127.0.0.1:5555 2>/dev/null | grep -Eq "connected to|already connected to"; then
|
|
72
|
-
BOOT=\$(adb -s 127.0.0.1:5555 shell getprop sys.boot_completed 2>/dev/null | tr -d '\\r' || true)
|
|
73
|
-
DEV_BOOT=\$(adb -s 127.0.0.1:5555 shell getprop dev.bootcomplete 2>/dev/null | tr -d '\\r' || true)
|
|
74
|
-
BOOT_ANIM=\$(adb -s 127.0.0.1:5555 shell getprop init.svc.bootanim 2>/dev/null | tr -d '\\r' || true)
|
|
75
|
-
if [ "\$BOOT" = "1" ] || [ "\$DEV_BOOT" = "1" ] || [ "\$BOOT_ANIM" = "stopped" ]; then
|
|
76
|
-
echo "[display] Android boot_completed=1"
|
|
77
|
-
break
|
|
78
|
-
fi
|
|
79
|
-
fi
|
|
80
|
-
sleep 3
|
|
81
|
-
done
|
|
82
|
-
|
|
83
|
-
echo "[display] Provisioning device..."
|
|
84
|
-
adb -s 127.0.0.1:5555 shell settings put global device_provisioned 1 2>/dev/null || true
|
|
85
|
-
adb -s 127.0.0.1:5555 shell settings put secure user_setup_complete 1 2>/dev/null || true
|
|
86
|
-
adb -s 127.0.0.1:5555 shell locksettings set-disabled true 2>/dev/null || true
|
|
87
|
-
adb -s 127.0.0.1:5555 shell wm dismiss-keyguard 2>/dev/null || true
|
|
88
|
-
adb -s 127.0.0.1:5555 shell input keyevent 82 2>/dev/null || true
|
|
89
|
-
|
|
90
|
-
echo "[display] Waiting for user unlock (RUNNING_UNLOCKED)..."
|
|
91
|
-
echo "[display] Note: vold deadlock will be broken by deployer via kubectl exec"
|
|
92
|
-
for i in \$(seq 1 120); do
|
|
93
|
-
USER_STATE=\$(adb -s 127.0.0.1:5555 shell "am get-started-user-state 0" 2>/dev/null | grep -o "RUNNING_[A-Z_]*" || true)
|
|
94
|
-
if [ "\$USER_STATE" = "RUNNING_UNLOCKED" ]; then
|
|
95
|
-
echo "[display] User 0 is RUNNING_UNLOCKED"
|
|
96
|
-
break
|
|
97
|
-
fi
|
|
98
|
-
if [ \$((i % 30)) -eq 0 ]; then
|
|
99
|
-
echo "[display] User state: \$USER_STATE (attempt \$i/120)"
|
|
100
|
-
fi
|
|
101
|
-
sleep 2
|
|
102
|
-
done
|
|
103
|
-
echo "[display] Android is ready (state: \$USER_STATE)"
|
|
104
|
-
|
|
105
|
-
echo "[display] Starting scrcpy supervisor loop"
|
|
106
|
-
SCRCPY_SERVER_PATH=\$(find /usr -name "scrcpy-server" -type f 2>/dev/null | head -1)
|
|
107
|
-
if [ -n "\$SCRCPY_SERVER_PATH" ]; then
|
|
108
|
-
export SCRCPY_SERVER_PATH
|
|
109
|
-
fi
|
|
110
|
-
|
|
111
|
-
while true; do
|
|
112
|
-
if adb connect 127.0.0.1:5555 2>/dev/null | grep -Eq "connected to|already connected to"; then
|
|
113
|
-
BOOT=\$(adb -s 127.0.0.1:5555 shell getprop sys.boot_completed 2>/dev/null | tr -d '\\r' || true)
|
|
114
|
-
DEV_BOOT=\$(adb -s 127.0.0.1:5555 shell getprop dev.bootcomplete 2>/dev/null | tr -d '\\r' || true)
|
|
115
|
-
BOOT_ANIM=\$(adb -s 127.0.0.1:5555 shell getprop init.svc.bootanim 2>/dev/null | tr -d '\\r' || true)
|
|
116
|
-
if [ "\$BOOT" = "1" ] || [ "\$DEV_BOOT" = "1" ] || [ "\$BOOT_ANIM" = "stopped" ]; then
|
|
117
|
-
echo "[display] Launching scrcpy..."
|
|
118
|
-
scrcpy --serial 127.0.0.1:5555 --max-fps 30 --bit-rate 4M --window-x 0 --window-y 0 --window-width 720 --window-height 1280 --window-borderless 2>&1 || true
|
|
119
|
-
echo "[display] scrcpy exited, retrying in 3s"
|
|
120
|
-
else
|
|
121
|
-
echo "[display] Android not fully booted yet (sys.boot_completed=\$BOOT, dev.bootcomplete=\$DEV_BOOT, bootanim=\$BOOT_ANIM)"
|
|
122
|
-
fi
|
|
123
|
-
else
|
|
124
|
-
echo "[display] adb connect not ready yet"
|
|
125
|
-
fi
|
|
126
|
-
sleep 3
|
|
127
|
-
done
|
|
128
|
-
`;
|
|
129
|
-
|
|
130
|
-
const HEALTH_CHECK = `#!/usr/bin/env bash
|
|
131
|
-
set -euo pipefail
|
|
132
|
-
MODE="\${1:-ready}"
|
|
133
|
-
|
|
134
|
-
if [ "\$MODE" = "startup" ]; then
|
|
135
|
-
pgrep -f "Xvfb" >/dev/null
|
|
136
|
-
pgrep -f "x11vnc" >/dev/null
|
|
137
|
-
exit 0
|
|
138
|
-
fi
|
|
139
|
-
|
|
140
|
-
curl -fsS --max-time 3 "http://127.0.0.1:6080/vnc.html" >/dev/null
|
|
141
|
-
adb start-server >/dev/null 2>&1 || true
|
|
142
|
-
adb connect 127.0.0.1:5555 >/dev/null 2>&1 || true
|
|
143
|
-
|
|
144
|
-
BOOT=\$(adb -s 127.0.0.1:5555 shell getprop sys.boot_completed 2>/dev/null | tr -d '\\r' || true)
|
|
145
|
-
DEV_BOOT=\$(adb -s 127.0.0.1:5555 shell getprop dev.bootcomplete 2>/dev/null | tr -d '\\r' || true)
|
|
146
|
-
BOOT_ANIM=\$(adb -s 127.0.0.1:5555 shell getprop init.svc.bootanim 2>/dev/null | tr -d '\\r' || true)
|
|
147
|
-
|
|
148
|
-
[ "\$BOOT" = "1" ] || [ "\$DEV_BOOT" = "1" ] || [ "\$BOOT_ANIM" = "stopped" ]
|
|
149
|
-
`;
|
|
150
|
-
|
|
151
|
-
const NOVNC_PORT = 6080;
|
|
152
55
|
|
|
153
56
|
function generateManifests(namespace, displayImage, domain, emulatorName) {
|
|
154
57
|
const core = [];
|
|
@@ -213,7 +116,8 @@ spec:
|
|
|
213
116
|
- androidboot.use_memfd=true
|
|
214
117
|
- androidboot.redroid_width=720
|
|
215
118
|
- androidboot.redroid_height=1280
|
|
216
|
-
- androidboot.redroid_fps=
|
|
119
|
+
- androidboot.redroid_fps=24
|
|
120
|
+
- androidboot.redroid_dpi=280
|
|
217
121
|
- androidboot.redroid_dex2oat_threads=1
|
|
218
122
|
- ro.crypto.state=unencrypted
|
|
219
123
|
- ro.crypto.type=none
|
|
@@ -240,27 +144,30 @@ spec:
|
|
|
240
144
|
capabilities:
|
|
241
145
|
add: ["NET_ADMIN"]
|
|
242
146
|
ports:
|
|
243
|
-
- containerPort: ${
|
|
244
|
-
name:
|
|
147
|
+
- containerPort: ${WS_SCRCPY_PORT}
|
|
148
|
+
name: ws-scrcpy
|
|
245
149
|
startupProbe:
|
|
246
|
-
|
|
247
|
-
|
|
150
|
+
httpGet:
|
|
151
|
+
path: /
|
|
152
|
+
port: ${WS_SCRCPY_PORT}
|
|
248
153
|
initialDelaySeconds: 30
|
|
249
154
|
periodSeconds: 10
|
|
250
|
-
timeoutSeconds:
|
|
155
|
+
timeoutSeconds: 5
|
|
251
156
|
failureThreshold: 60
|
|
252
157
|
readinessProbe:
|
|
253
|
-
|
|
254
|
-
|
|
158
|
+
httpGet:
|
|
159
|
+
path: /
|
|
160
|
+
port: ${WS_SCRCPY_PORT}
|
|
255
161
|
periodSeconds: 15
|
|
256
|
-
timeoutSeconds:
|
|
162
|
+
timeoutSeconds: 5
|
|
257
163
|
failureThreshold: 5
|
|
258
164
|
livenessProbe:
|
|
259
|
-
|
|
260
|
-
|
|
165
|
+
httpGet:
|
|
166
|
+
path: /
|
|
167
|
+
port: ${WS_SCRCPY_PORT}
|
|
261
168
|
initialDelaySeconds: 60
|
|
262
169
|
periodSeconds: 30
|
|
263
|
-
timeoutSeconds:
|
|
170
|
+
timeoutSeconds: 5
|
|
264
171
|
failureThreshold: 10
|
|
265
172
|
volumes:
|
|
266
173
|
- name: redroid-data
|
|
@@ -281,9 +188,9 @@ spec:
|
|
|
281
188
|
app: ${emulatorName}
|
|
282
189
|
ports:
|
|
283
190
|
- protocol: TCP
|
|
284
|
-
port: ${
|
|
285
|
-
targetPort: ${
|
|
286
|
-
name:
|
|
191
|
+
port: ${WS_SCRCPY_PORT}
|
|
192
|
+
targetPort: ${WS_SCRCPY_PORT}
|
|
193
|
+
name: ws-scrcpy`);
|
|
287
194
|
|
|
288
195
|
if (domain) {
|
|
289
196
|
ingress.push(`apiVersion: networking.k8s.io/v1
|
|
@@ -293,10 +200,11 @@ metadata:
|
|
|
293
200
|
namespace: ${namespace}
|
|
294
201
|
annotations:
|
|
295
202
|
cert-manager.io/cluster-issuer: letsencrypt-production
|
|
296
|
-
nginx.ingress.kubernetes.io/
|
|
203
|
+
nginx.ingress.kubernetes.io/proxy-body-size: "0"
|
|
297
204
|
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
|
298
205
|
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
|
|
299
206
|
nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
|
|
207
|
+
nginx.ingress.kubernetes.io/websocket-services: "${emulatorName}"
|
|
300
208
|
spec:
|
|
301
209
|
ingressClassName: nginx
|
|
302
210
|
tls:
|
|
@@ -313,7 +221,7 @@ spec:
|
|
|
313
221
|
service:
|
|
314
222
|
name: ${emulatorName}
|
|
315
223
|
port:
|
|
316
|
-
number: ${
|
|
224
|
+
number: ${WS_SCRCPY_PORT}`);
|
|
317
225
|
}
|
|
318
226
|
|
|
319
227
|
return { core: core.join('\n---\n'), ingress: ingress.length ? ingress.join('\n---\n') : null };
|
|
@@ -331,8 +239,8 @@ function createWebAndroidEmulatorAdapter(adapterOptions = {}) {
|
|
|
331
239
|
|
|
332
240
|
const previewUrl = adapterOptions.previewUrl ||
|
|
333
241
|
(domain
|
|
334
|
-
? `https://${domain}
|
|
335
|
-
: `http://localhost:${
|
|
242
|
+
? `https://${domain}`
|
|
243
|
+
: `http://localhost:${WS_SCRCPY_PORT}`);
|
|
336
244
|
|
|
337
245
|
return {
|
|
338
246
|
async start({ runner, workspacePath, artifactPath, applicationId, onLog, projectSlug, domain: projectDomain, cloudflareDns: projectCloudflareDns }) {
|
|
@@ -342,66 +250,22 @@ function createWebAndroidEmulatorAdapter(adapterOptions = {}) {
|
|
|
342
250
|
const effectiveDomain = projectDomain || domain;
|
|
343
251
|
const effectiveCloudflareDns = projectCloudflareDns || cloudflareDns;
|
|
344
252
|
|
|
345
|
-
/* ── 1.
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
.update(DISPLAY_ENTRYPOINT)
|
|
349
|
-
.update(HEALTH_CHECK)
|
|
350
|
-
.digest('hex')
|
|
351
|
-
.slice(0, 12);
|
|
352
|
-
const imageTag = `redroid-display:${buildHash}`;
|
|
353
|
-
const registryImage = `${registryHost}/${imageTag}`;
|
|
253
|
+
/* ── 1. Use pre-built ws-scrcpy image ────────────────────────────── */
|
|
254
|
+
const registryImage = `${registryHost}/${WS_SCRCPY_IMAGE}`;
|
|
255
|
+
await emit(`[redroid] Using ws-scrcpy display image: ${registryImage}`);
|
|
354
256
|
|
|
355
257
|
const checkResult = await runner.run(
|
|
356
|
-
`docker images -q ${
|
|
258
|
+
`docker images -q ${registryHost}/${WS_SCRCPY_IMAGE} 2>/dev/null || sudo docker images -q ${registryHost}/${WS_SCRCPY_IMAGE} 2>/dev/null`,
|
|
357
259
|
{ allowFailure: true },
|
|
358
260
|
);
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
await runner.writeBase64File(
|
|
365
|
-
path.posix.join(buildDir, 'Dockerfile'),
|
|
366
|
-
Buffer.from(DISPLAY_DOCKERFILE).toString('base64'),
|
|
367
|
-
);
|
|
368
|
-
await runner.writeBase64File(
|
|
369
|
-
path.posix.join(buildDir, 'entrypoint.sh'),
|
|
370
|
-
Buffer.from(DISPLAY_ENTRYPOINT).toString('base64'),
|
|
371
|
-
);
|
|
372
|
-
await runner.writeBase64File(
|
|
373
|
-
path.posix.join(buildDir, 'health-check.sh'),
|
|
374
|
-
Buffer.from(HEALTH_CHECK).toString('base64'),
|
|
261
|
+
if (!checkResult.stdout.trim()) {
|
|
262
|
+
await emit('[redroid] ws-scrcpy image not found in registry, tagging and pushing...');
|
|
263
|
+
await runner.run(
|
|
264
|
+
`(docker tag ${WS_SCRCPY_IMAGE} ${registryImage} && docker push ${registryImage}) || (sudo docker tag ${WS_SCRCPY_IMAGE} ${registryImage} && sudo docker push ${registryImage})`,
|
|
265
|
+
{ onStdout: emit, onStderr: emit },
|
|
375
266
|
);
|
|
376
|
-
await runner.run(`chmod +x ${buildDir}/entrypoint.sh ${buildDir}/health-check.sh`);
|
|
377
|
-
|
|
378
|
-
let lastOutputAt = Date.now();
|
|
379
|
-
const heartbeat = setInterval(async () => {
|
|
380
|
-
const s = Math.floor((Date.now() - lastOutputAt) / 1000);
|
|
381
|
-
if (s >= 30) await emit(`[redroid] still building... (${s}s)`);
|
|
382
|
-
}, 30 * 1000);
|
|
383
|
-
const track = (line) => { lastOutputAt = Date.now(); return emit(line); };
|
|
384
|
-
try {
|
|
385
|
-
await runner.run(
|
|
386
|
-
`docker build --provenance=false --sbom=false -t ${imageTag} ${buildDir} || sudo docker build --provenance=false --sbom=false -t ${imageTag} ${buildDir}`,
|
|
387
|
-
{ onStdout: track, onStderr: track, timeoutMs: 30 * 60 * 1000 },
|
|
388
|
-
);
|
|
389
|
-
} finally {
|
|
390
|
-
clearInterval(heartbeat);
|
|
391
|
-
}
|
|
392
|
-
await emit('[redroid] Display sidecar built');
|
|
393
|
-
} else {
|
|
394
|
-
await emit('[redroid] Display sidecar image exists, skipping build');
|
|
395
267
|
}
|
|
396
268
|
|
|
397
|
-
/* ── 2. Push display sidecar ─────────────────────────────────────── */
|
|
398
|
-
await emit(`[redroid] Pushing display sidecar to ${registryImage}...`);
|
|
399
|
-
await runner.run(
|
|
400
|
-
`(docker tag ${imageTag} ${registryImage} && docker push ${registryImage}) || (sudo docker tag ${imageTag} ${registryImage} && sudo docker push ${registryImage})`,
|
|
401
|
-
{ onStdout: emit, onStderr: emit },
|
|
402
|
-
);
|
|
403
|
-
await emit('[redroid] Image pushed');
|
|
404
|
-
|
|
405
269
|
/* ── 3. Ensure redroid image is available on nodes ───────────────── */
|
|
406
270
|
await emit('[redroid] Pre-pulling redroid image on nodes...');
|
|
407
271
|
try {
|
|
@@ -511,6 +375,27 @@ function createWebAndroidEmulatorAdapter(adapterOptions = {}) {
|
|
|
511
375
|
}
|
|
512
376
|
await emit('[redroid] Pod is ready');
|
|
513
377
|
|
|
378
|
+
/* ── 5.4. Inject custom stream index.html ──────────────────────── */
|
|
379
|
+
if (effectiveDomain) {
|
|
380
|
+
try {
|
|
381
|
+
const podForHtml = await runner.run(
|
|
382
|
+
`kubectl get pod -n ${namespace} -l app=${emulatorName} -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || sudo kubectl get pod -n ${namespace} -l app=${emulatorName} -o jsonpath='{.items[0].metadata.name}'`,
|
|
383
|
+
);
|
|
384
|
+
const htmlPodName = podForHtml.stdout.trim().replace(/'/g, '');
|
|
385
|
+
if (htmlPodName) {
|
|
386
|
+
await emit('[redroid] Injecting custom stream UI (auto-redirect, centered display)...');
|
|
387
|
+
const indexHtml = generateStreamIndexHtml();
|
|
388
|
+
const indexB64 = Buffer.from(indexHtml).toString('base64');
|
|
389
|
+
await runner.run(
|
|
390
|
+
`echo '${indexB64}' | kubectl exec -n ${namespace} ${htmlPodName} -c display -i -- sh -c 'base64 -d > /app/public/index.html' || echo '${indexB64}' | sudo kubectl exec -n ${namespace} ${htmlPodName} -c display -i -- sh -c 'base64 -d > /app/public/index.html'`,
|
|
391
|
+
);
|
|
392
|
+
await emit('[redroid] Custom stream UI injected');
|
|
393
|
+
}
|
|
394
|
+
} catch (err) {
|
|
395
|
+
await emit(`[redroid] Custom UI injection failed (non-fatal): ${err.message || err}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
514
399
|
/* ── 5.5. Fix Android policy routing ────────────────────────────── */
|
|
515
400
|
{
|
|
516
401
|
const fixPodResult = await runner.run(
|
|
@@ -690,7 +575,7 @@ exit 0'`,
|
|
|
690
575
|
}
|
|
691
576
|
|
|
692
577
|
const projectPreviewUrl = effectiveDomain
|
|
693
|
-
? `https://${effectiveDomain}
|
|
578
|
+
? `https://${effectiveDomain}`
|
|
694
579
|
: previewUrl;
|
|
695
580
|
await emit(`[redroid] Preview ready: ${projectPreviewUrl}`);
|
|
696
581
|
return { url: projectPreviewUrl };
|