fix(windows/web): start the console once its inputs exist, and verify it started
arch / build-publish (push) Successful in 17m1s
ci / web (push) Successful in 53s
ci / docs-site (push) Successful in 58s
apple / swift (push) Successful in 1m19s
decky / build-publish (push) Successful in 24s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 35s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 47s
ci / bench (push) Successful in 6m19s
deb / build-publish (push) Successful in 9m35s
docker / deploy-docs (push) Successful in 29s
apple / screenshots (push) Successful in 10m56s
deb / build-publish-host (push) Successful in 13m40s
android / android (push) Successful in 16m19s
windows-host / package (push) Successful in 17m13s
ci / rust (push) Successful in 19m39s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m44s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 20m18s
arch / build-publish (push) Successful in 17m1s
ci / web (push) Successful in 53s
ci / docs-site (push) Successful in 58s
apple / swift (push) Successful in 1m19s
decky / build-publish (push) Successful in 24s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 35s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 47s
ci / bench (push) Successful in 6m19s
deb / build-publish (push) Successful in 9m35s
docker / deploy-docs (push) Successful in 29s
apple / screenshots (push) Successful in 10m56s
deb / build-publish-host (push) Successful in 13m40s
android / android (push) Successful in 16m19s
windows-host / package (push) Successful in 17m13s
ci / rust (push) Successful in 19m39s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m44s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 20m18s
A fresh install left PunktfunkWeb registered but not running: `web setup` waited only for the mgmt token before firing `schtasks /run`, while `web-run.cmd` also requires the host identity cert — and the host writes the token during argument parsing but `cert.pem` only after the pure-Rust RSA-2048 keygen inside `serve`. The launcher lost that race, exited 1, and since the task carried no trigger but boot (Task Scheduler does not reliably restart on a non-zero exit code) the console stayed down until the next reboot, with the installer still reporting "web console set up + started". - `web setup` gates on cert.pem (written last) as well as the token, 90 s budget. - After `schtasks /run`, poll for the :47992 listener and retry before giving up; warn honestly instead of claiming a start that did not happen. - `web-run.cmd` (installed + dev) waits in-process for the token + cert (~5 min) rather than exiting 1 and hoping restart-on-failure retries. - Register the task with a logon trigger alongside boot, falling back to the boot-only XML if a Task Scheduler build rejects it. - Linux had the same defect: punktfunk-web.service's Restart=on-failure gave up permanently after systemd's default 5-starts-in-10 s limit. StartLimitIntervalSec=0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -431,16 +431,69 @@ fn web_setup(args: &[String]) -> Result<()> {
|
|||||||
) {
|
) {
|
||||||
eprintln!("warning: could not add the firewall rule for TCP 47992");
|
eprintln!("warning: could not add the firewall rule for TCP 47992");
|
||||||
}
|
}
|
||||||
// 5. wait briefly for the host's mgmt token, then start (restart-on-failure picks it up otherwise)
|
// 5. Wait for EVERY file the launcher needs, then start the task and VERIFY it came up.
|
||||||
for _ in 0..30 {
|
//
|
||||||
if token_path.exists() {
|
// `web-run.cmd` refuses to serve without the mgmt token AND the host identity cert, and the
|
||||||
break;
|
// host writes them in that order: the token during argument parsing (`main::parse_serve`,
|
||||||
|
// milliseconds after `serve` starts), the cert only once `serve` reaches
|
||||||
|
// `gamestream::cert::ServerIdentity::load_or_create` - behind a pure-Rust RSA-2048 keygen
|
||||||
|
// whose prime search has a multi-second tail. Gating on the token alone therefore fired
|
||||||
|
// `schtasks /run` while that keygen was still running: the launcher exited 1, and since the
|
||||||
|
// task carries no trigger other than boot (and Task Scheduler's restart-on-failure does not
|
||||||
|
// reliably fire for a plain non-zero exit code), a freshly installed console stayed down
|
||||||
|
// until the next reboot. Gate on `cert.pem` - written LAST, after `key.pem` - so all three
|
||||||
|
// files are on disk before the first start.
|
||||||
|
let cert_path = data_dir.join("cert.pem");
|
||||||
|
if !wait_for_files(&[token_path.as_path(), cert_path.as_path()], 90) {
|
||||||
|
eprintln!(
|
||||||
|
"warning: the host service has not written {} + {} yet - not starting the console now; \
|
||||||
|
it will start at the next boot (or run: schtasks /run /tn {WEB_TASK})",
|
||||||
|
token_path.display(),
|
||||||
|
cert_path.display()
|
||||||
|
);
|
||||||
|
println!("web console set up (https://<host-ip>:47992)");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if start_web_task() {
|
||||||
|
println!("web console set up + started (https://<host-ip>:47992)");
|
||||||
|
} else {
|
||||||
|
// Never claim "started" when it isn't: the launcher's own diagnostics go to a task with no
|
||||||
|
// console, so this warning is the only trace an operator gets at install time.
|
||||||
|
eprintln!(
|
||||||
|
"warning: the {WEB_TASK} task did not bring up a listener on :47992 - check its Last Run \
|
||||||
|
Result in Task Scheduler; the console will be retried at the next boot"
|
||||||
|
);
|
||||||
|
println!("web console set up (https://<host-ip>:47992)");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Poll until every path exists, or `secs` elapse. Returns whether they all showed up.
|
||||||
|
fn wait_for_files(paths: &[&Path], secs: u64) -> bool {
|
||||||
|
for _ in 0..secs {
|
||||||
|
if paths.iter().all(|p| p.exists()) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||||
}
|
}
|
||||||
|
paths.iter().all(|p| p.exists())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `schtasks /run` + verify: the command reports only that a start was *attempted*, so poll for the
|
||||||
|
/// console's own listener and retry a couple of times before giving up. Returns whether :47992 ended
|
||||||
|
/// up listening.
|
||||||
|
fn start_web_task() -> bool {
|
||||||
|
for _ in 0..3 {
|
||||||
run_quiet("schtasks", &["/run", "/tn", WEB_TASK]);
|
run_quiet("schtasks", &["/run", "/tn", WEB_TASK]);
|
||||||
println!("web console set up + started (https://<host-ip>:47992)");
|
// Bun binds the port a second or two after launch on a warm box; allow for a cold one.
|
||||||
Ok(())
|
for _ in 0..10 {
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||||
|
if !web_listener_pids().is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Source: a non-empty `--password-file` (fresh install) > keep existing (upgrade) > random fallback.
|
/// Source: a non-empty `--password-file` (fresh install) > keep existing (upgrade) > random fallback.
|
||||||
@@ -505,30 +558,57 @@ fn random_password() -> String {
|
|||||||
/// ("LISTENING"/"ABHOEREN"/...) is never parsed.
|
/// ("LISTENING"/"ABHOEREN"/...) is never parsed.
|
||||||
fn stop_web_console() {
|
fn stop_web_console() {
|
||||||
run_quiet("schtasks", &["/end", "/tn", WEB_TASK]);
|
run_quiet("schtasks", &["/end", "/tn", WEB_TASK]);
|
||||||
for line in run_capture("netstat", &["-ano", "-p", "tcp"]).lines() {
|
for pid in web_listener_pids() {
|
||||||
let toks: Vec<&str> = line.split_whitespace().collect();
|
run_quiet("taskkill", &["/PID", &pid, "/F"]);
|
||||||
if toks.len() >= 5
|
|
||||||
&& toks[0].eq_ignore_ascii_case("tcp")
|
|
||||||
&& toks[1].ends_with(":47992")
|
|
||||||
&& (toks[2] == "0.0.0.0:0" || toks[2] == "[::]:0")
|
|
||||||
{
|
|
||||||
let pid = toks[toks.len() - 1];
|
|
||||||
if !pid.is_empty() && pid.bytes().all(|b| b.is_ascii_digit()) {
|
|
||||||
run_quiet("taskkill", &["/PID", pid, "/F"]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Register the boot/SYSTEM/restart-on-failure task via a generated Task Scheduler XML (`schtasks /xml`,
|
/// PIDs owning a LISTEN socket on :47992. Also the console's liveness probe (`start_web_task`).
|
||||||
/// no COM). The XML declares UTF-16, so it's written UTF-16LE+BOM.
|
fn web_listener_pids() -> Vec<String> {
|
||||||
|
run_capture("netstat", &["-ano", "-p", "tcp"])
|
||||||
|
.lines()
|
||||||
|
.filter_map(|line| {
|
||||||
|
let toks: Vec<&str> = line.split_whitespace().collect();
|
||||||
|
let listening = toks.len() >= 5
|
||||||
|
&& toks[0].eq_ignore_ascii_case("tcp")
|
||||||
|
&& toks[1].ends_with(":47992")
|
||||||
|
&& (toks[2] == "0.0.0.0:0" || toks[2] == "[::]:0");
|
||||||
|
let pid = *toks.last()?;
|
||||||
|
(listening && !pid.is_empty() && pid.bytes().all(|b| b.is_ascii_digit()))
|
||||||
|
.then(|| pid.to_string())
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register the boot+logon/SYSTEM/restart-on-failure task via a generated Task Scheduler XML
|
||||||
|
/// (`schtasks /xml`, no COM). The XML declares UTF-16, so it's written UTF-16LE+BOM.
|
||||||
|
///
|
||||||
|
/// Two triggers, because boot alone left too many holes: a console that lost the install-time start
|
||||||
|
/// (or a box where the host service was started only later) had to wait for a full reboot. Logon is
|
||||||
|
/// free insurance - `MultipleInstancesPolicy=IgnoreNew` makes a redundant start a no-op. If a
|
||||||
|
/// Task Scheduler build rejects the two-trigger XML we fall back to the boot-only form rather than
|
||||||
|
/// fail registration outright (no task at all is strictly worse than a boot-only task).
|
||||||
fn register_web_task(cmd: &Path) -> Result<()> {
|
fn register_web_task(cmd: &Path) -> Result<()> {
|
||||||
|
let cmd_xml = xml_escape(&cmd.to_string_lossy());
|
||||||
|
let boot = "<BootTrigger><Enabled>true</Enabled></BootTrigger>";
|
||||||
|
let boot_and_logon = "<BootTrigger><Enabled>true</Enabled></BootTrigger>\
|
||||||
|
<LogonTrigger><Enabled>true</Enabled></LogonTrigger>";
|
||||||
|
if try_register_web_task(&cmd_xml, boot_and_logon) || try_register_web_task(&cmd_xml, boot) {
|
||||||
|
println!("registered scheduled task {WEB_TASK} -> {}", cmd.display());
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
bail!("schtasks /create {WEB_TASK} failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One `schtasks /create /xml` attempt with the given `<Triggers>` body. Returns whether it took.
|
||||||
|
fn try_register_web_task(cmd_xml: &str, triggers: &str) -> bool {
|
||||||
let xml = format!(
|
let xml = format!(
|
||||||
"<?xml version=\"1.0\" encoding=\"UTF-16\"?>\n\
|
"<?xml version=\"1.0\" encoding=\"UTF-16\"?>\n\
|
||||||
<Task version=\"1.2\" xmlns=\"http://schemas.microsoft.com/windows/2004/02/mit/task\">\n\
|
<Task version=\"1.2\" xmlns=\"http://schemas.microsoft.com/windows/2004/02/mit/task\">\n\
|
||||||
<RegistrationInfo><Description>punktfunk web management console (Nitro SSR on bun, :47992)</Description></RegistrationInfo>\n\
|
<RegistrationInfo><Description>punktfunk web management console (Nitro SSR on bun, :47992)</Description></RegistrationInfo>\n\
|
||||||
<Triggers><BootTrigger><Enabled>true</Enabled></BootTrigger></Triggers>\n\
|
<Triggers>{triggers}</Triggers>\n\
|
||||||
<Principals><Principal id=\"Author\"><UserId>S-1-5-18</UserId><RunLevel>HighestAvailable</RunLevel></Principal></Principals>\n\
|
<Principals><Principal id=\"Author\"><UserId>S-1-5-18</UserId><RunLevel>HighestAvailable</RunLevel></Principal></Principals>\n\
|
||||||
<Settings>\n\
|
<Settings>\n\
|
||||||
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>\n\
|
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>\n\
|
||||||
@@ -538,12 +618,13 @@ fn register_web_task(cmd: &Path) -> Result<()> {
|
|||||||
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>\n\
|
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>\n\
|
||||||
<RestartOnFailure><Interval>PT1M</Interval><Count>10</Count></RestartOnFailure>\n\
|
<RestartOnFailure><Interval>PT1M</Interval><Count>10</Count></RestartOnFailure>\n\
|
||||||
</Settings>\n\
|
</Settings>\n\
|
||||||
<Actions Context=\"Author\"><Exec><Command>{}</Command></Exec></Actions>\n\
|
<Actions Context=\"Author\"><Exec><Command>{cmd_xml}</Command></Exec></Actions>\n\
|
||||||
</Task>",
|
</Task>"
|
||||||
xml_escape(&cmd.to_string_lossy())
|
|
||||||
);
|
);
|
||||||
let xml_path = std::env::temp_dir().join("punktfunk-web-task.xml");
|
let xml_path = std::env::temp_dir().join("punktfunk-web-task.xml");
|
||||||
write_utf16le_bom(&xml_path, &xml)?;
|
if write_utf16le_bom(&xml_path, &xml).is_err() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
let ok = run_quiet(
|
let ok = run_quiet(
|
||||||
"schtasks",
|
"schtasks",
|
||||||
&[
|
&[
|
||||||
@@ -556,12 +637,7 @@ fn register_web_task(cmd: &Path) -> Result<()> {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
let _ = std::fs::remove_file(&xml_path);
|
let _ = std::fs::remove_file(&xml_path);
|
||||||
if ok {
|
ok
|
||||||
println!("registered scheduled task {WEB_TASK} -> {}", cmd.display());
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
bail!("schtasks /create {WEB_TASK} failed")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_utf16le_bom(path: &Path, s: &str) -> Result<()> {
|
fn write_utf16le_bom(path: &Path, s: &str) -> Result<()> {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ the Windows specifics follow.
|
|||||||
|
|
||||||
The installer also sets up the **web management console** (status, paired devices, the PIN pairing
|
The installer also sets up the **web management console** (status, paired devices, the PIN pairing
|
||||||
flow): it bundles the console plus its own runtime and runs it as the **`PunktfunkWeb`** task on
|
flow): it bundles the console plus its own runtime and runs it as the **`PunktfunkWeb`** task on
|
||||||
**`https://<this-PC>:47992`**, starting at boot.
|
**`https://<this-PC>:47992`**, starting at boot and at sign-in.
|
||||||
|
|
||||||
#### Console login password
|
#### Console login password
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ Description=punktfunk management web console
|
|||||||
# web-init generates the login password; the host writes the mgmt token. Order after both.
|
# web-init generates the login password; the host writes the mgmt token. Order after both.
|
||||||
After=punktfunk-web-init.service punktfunk-host.service
|
After=punktfunk-web-init.service punktfunk-host.service
|
||||||
Wants=punktfunk-web-init.service
|
Wants=punktfunk-web-init.service
|
||||||
|
# Retry indefinitely while the host is still writing the mgmt token + identity cert. Without this,
|
||||||
|
# systemd's default rate limit (5 starts / 10 s) plus RestartSec=2 gives up permanently after ~10 s
|
||||||
|
# - so a console enabled before the host's first run stayed dead until someone restarted it by hand
|
||||||
|
# (the same defect the Windows PunktfunkWeb task had).
|
||||||
|
StartLimitIntervalSec=0
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
|
|||||||
@@ -16,17 +16,26 @@ set "CERTFILE=%PFDATA%\cert.pem"
|
|||||||
set "KEYFILE=%PFDATA%\key.pem"
|
set "KEYFILE=%PFDATA%\key.pem"
|
||||||
|
|
||||||
rem The host's `serve` writes the mgmt token + its identity cert/key on first run. Until they exist
|
rem The host's `serve` writes the mgmt token + its identity cert/key on first run. Until they exist
|
||||||
rem we have no credential and no TLS material, so fail and let the task's restart-on-failure retry
|
rem we have no credential and no TLS material, so WAIT rather than silently downgrading to plain HTTP.
|
||||||
rem (mirrors the Linux unit's Restart=on-failure waiting for the host to create them) rather than
|
rem
|
||||||
rem silently downgrading to plain HTTP.
|
rem Wait in-process instead of exiting 1 and hoping the task's restart-on-failure retries: Task
|
||||||
if not exist "%TOKENFILE%" (
|
rem Scheduler does not reliably restart on a plain non-zero exit code, so a console that started
|
||||||
echo [punktfunk-web] mgmt token not present yet at "%TOKENFILE%" - waiting for the host service.
|
rem before the host finished writing those files (the token lands at argument parse, the cert only
|
||||||
exit /b 1
|
rem after the RSA-2048 keygen) used to stay down until the next reboot. ~5 min at 2 s, then give up
|
||||||
)
|
rem so a genuinely broken install still surfaces as a failed task rather than one that runs forever.
|
||||||
if not exist "%CERTFILE%" (
|
rem `timeout` needs a console this task does not have, so `ping -n 3` is the 2-second sleep.
|
||||||
echo [punktfunk-web] host identity cert not present yet at "%CERTFILE%" - waiting for the host service.
|
set /a PFWAITS=0
|
||||||
|
:pfwait
|
||||||
|
if exist "%TOKENFILE%" if exist "%CERTFILE%" goto pfready
|
||||||
|
if %PFWAITS% GEQ 150 (
|
||||||
|
echo [punktfunk-web] gave up waiting for "%TOKENFILE%" + "%CERTFILE%" - is the punktfunk host service running?
|
||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
if %PFWAITS%==0 echo [punktfunk-web] waiting for the host service to write the mgmt token + identity cert...
|
||||||
|
set /a PFWAITS+=1
|
||||||
|
ping -n 3 127.0.0.1 >nul 2>&1
|
||||||
|
goto pfwait
|
||||||
|
:pfready
|
||||||
|
|
||||||
rem Both files are single KEY=VALUE lines (LF), written 0600/ACL'd: PUNKTFUNK_MGMT_TOKEN=... and
|
rem Both files are single KEY=VALUE lines (LF), written 0600/ACL'd: PUNKTFUNK_MGMT_TOKEN=... and
|
||||||
rem PUNKTFUNK_UI_PASSWORD=... . Split on the first '=' and import each into the environment.
|
rem PUNKTFUNK_UI_PASSWORD=... . Split on the first '=' and import each into the environment.
|
||||||
|
|||||||
+13
-8
@@ -18,16 +18,21 @@ set "CERTFILE=%PFDATA%\cert.pem"
|
|||||||
set "KEYFILE=%PFDATA%\key.pem"
|
set "KEYFILE=%PFDATA%\key.pem"
|
||||||
|
|
||||||
rem The host's `serve` writes the mgmt token + identity cert on first run. Until they exist the proxy
|
rem The host's `serve` writes the mgmt token + identity cert on first run. Until they exist the proxy
|
||||||
rem has no credential and no TLS material, so fail and let restart-on-failure retry (mirrors the
|
rem has no credential and no TLS material, so WAIT for them (mirrors the installed launcher) rather
|
||||||
rem installed launcher / Linux unit) rather than silently serving plain HTTP.
|
rem than silently serving plain HTTP - see scripts\windows\web-run.cmd for why waiting here beats
|
||||||
if not exist "%TOKENFILE%" (
|
rem exiting 1 and relying on the task's restart-on-failure. ~5 min at 2 s, then give up.
|
||||||
echo [punktfunk-web] mgmt token not present yet at "%TOKENFILE%" - waiting for the host service.
|
set /a PFWAITS=0
|
||||||
exit /b 1
|
:pfwait
|
||||||
)
|
if exist "%TOKENFILE%" if exist "%CERTFILE%" goto pfready
|
||||||
if not exist "%CERTFILE%" (
|
if %PFWAITS% GEQ 150 (
|
||||||
echo [punktfunk-web] host identity cert not present yet at "%CERTFILE%" - waiting for the host service.
|
echo [punktfunk-web] gave up waiting for "%TOKENFILE%" + "%CERTFILE%" - is the punktfunk host running?
|
||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
if %PFWAITS%==0 echo [punktfunk-web] waiting for the host to write the mgmt token + identity cert...
|
||||||
|
set /a PFWAITS+=1
|
||||||
|
ping -n 3 127.0.0.1 >nul 2>&1
|
||||||
|
goto pfwait
|
||||||
|
:pfready
|
||||||
|
|
||||||
rem Both files are single KEY=VALUE lines: PUNKTFUNK_MGMT_TOKEN=... and PUNKTFUNK_UI_PASSWORD=... .
|
rem Both files are single KEY=VALUE lines: PUNKTFUNK_MGMT_TOKEN=... and PUNKTFUNK_UI_PASSWORD=... .
|
||||||
rem Split on the first '=' and import each into the environment.
|
rem Split on the first '=' and import each into the environment.
|
||||||
|
|||||||
Reference in New Issue
Block a user