@techierathore/techieflow 1.0.4 → 1.1.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/.claude/commands/TechieFlow/agents/flow-master.md +2 -0
- package/.claude/commands/TechieFlow/agents/verifier.md +2 -2
- package/.claude/commands/TechieFlow/tasks/_develop-mode.md +34 -0
- package/.claude/commands/TechieFlow/tasks/_owner-language.md +1 -0
- package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +4 -3
- package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +2 -2
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +2 -2
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +3 -3
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +5 -5
- package/.claude/commands/TechieFlow/tasks/develop-end-to-end.md +30 -0
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +3 -2
- package/.claude/commands/TechieFlow/tasks/generate-html.md +2 -2
- package/.claude/commands/TechieFlow/tasks/handoff-phase.md +2 -2
- package/.claude/commands/TechieFlow/tasks/mockups.md +2 -2
- package/.claude/commands/TechieFlow/tasks/productguide.md +2 -2
- package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +1 -1
- package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +2 -2
- package/.claude/commands/TechieFlow/tasks/triage-issues.md +2 -2
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +12 -11
- package/.opencode/plugin/techieflow.js +194 -14
- package/.tfcore/agents/flow-master.md +2 -0
- package/.tfcore/agents/verifier.md +2 -2
- package/.tfcore/core-config.yaml +2 -1
- package/.tfcore/hooks/block-git.sh +4 -2
- package/.tfcore/hooks/guard-build.sh +5 -1
- package/.tfcore/hooks/guard-db.sh +112 -13
- package/.tfcore/hooks/guard-status-html.sh +86 -2
- package/.tfcore/hooks/guard-verify-deps.sh +16 -1
- package/.tfcore/hooks/guard-verify.sh +8 -3
- package/.tfcore/hooks/sweep-artifacts.sh +46 -0
- package/.tfcore/hooks/yolo-allow.sh +27 -0
- package/.tfcore/standards/blazor-zoom.cs.txt +191 -0
- package/.tfcore/standards/coding-standards-dotnet.md +31 -3
- package/.tfcore/standards/owner-words.txt +23 -0
- package/.tfcore/tasks/_develop-mode.md +34 -0
- package/.tfcore/tasks/_owner-language.md +1 -0
- package/.tfcore/tasks/_status-update-gate.md +4 -3
- package/.tfcore/tasks/_yolo-mode.md +2 -2
- package/.tfcore/tasks/amend-docs.md +2 -2
- package/.tfcore/tasks/day1-brownfield.md +3 -3
- package/.tfcore/tasks/day1-greenfield.md +5 -5
- package/.tfcore/tasks/develop-end-to-end.md +30 -0
- package/.tfcore/tasks/fix-issues.md +3 -2
- package/.tfcore/tasks/generate-html.md +2 -2
- package/.tfcore/tasks/handoff-phase.md +2 -2
- package/.tfcore/tasks/mockups.md +2 -2
- package/.tfcore/tasks/productguide.md +2 -2
- package/.tfcore/tasks/render-workflow-docs.md +1 -1
- package/.tfcore/tasks/triage-and-fix.md +2 -2
- package/.tfcore/tasks/triage-issues.md +2 -2
- package/.tfcore/tasks/verify-phase.md +12 -11
- package/.tfcore/telemetry/SCHEMA.md +10 -2
- package/.tfcore/telemetry/install-metrics.sh +3 -2
- package/.tfcore/telemetry/tf-metrics.sh +38 -2
- package/.tfcore/templates/stack-defaults/dotnet.md +3 -1
- package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +6 -0
- package/.tfcore/templates/v4custom/app-architecture-tmpl.md +5 -0
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +7 -4
- package/.tfcore/templates/v4custom/app-checklist-tmpl.md +11 -0
- package/.tfcore/templates/v4custom/app-phase-brd-tmpl.md +94 -0
- package/.tfcore/templates/v4custom/app-phase-uidesign-tmpl.md +60 -0
- package/.tfcore/templates/v4custom/app-phases-tmpl.md +2 -1
- package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +4 -2
- package/.tfcore/templates/v4custom/build-subagent-prompt.md +2 -2
- package/.tfcore/templates/v4custom/html-render-shell.md +6 -2
- package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/tf-appium.mjs +179 -0
- package/.tfcore/utils/tf-assets-browser.mjs +96 -0
- package/.tfcore/utils/tf-assets.sh +97 -12
- package/.tfcore/utils/tf-brd-status.py +14 -5
- package/.tfcore/utils/tf-build-list.py +95 -12
- package/.tfcore/utils/tf-build-list.sh +2 -1
- package/.tfcore/utils/tf-build.sh +163 -15
- package/.tfcore/utils/tf-checklist-edit.py +21 -3
- package/.tfcore/utils/tf-ci-repro.py +685 -0
- package/.tfcore/utils/tf-ci-repro.sh +24 -0
- package/.tfcore/utils/tf-day1-files.py +55 -28
- package/.tfcore/utils/tf-develop.sh +264 -0
- package/.tfcore/utils/tf-devguide-list.py +53 -24
- package/.tfcore/utils/tf-doc-check.py +218 -26
- package/.tfcore/utils/tf-doc-tests.sh +116 -0
- package/.tfcore/utils/tf-emit.sh +132 -45
- package/.tfcore/utils/tf-feedback.sh +13 -0
- package/.tfcore/utils/tf-fix-close.sh +87 -31
- package/.tfcore/utils/tf-goal.sh +12 -12
- package/.tfcore/utils/tf-harness.sh +9 -4
- package/.tfcore/utils/tf-lock.sh +29 -0
- package/.tfcore/utils/tf-log-miss.py +34 -8
- package/.tfcore/utils/tf-login.mjs +167 -0
- package/.tfcore/utils/tf-maccatalyst-check.sh +92 -0
- package/.tfcore/utils/tf-menus.mjs +401 -0
- package/.tfcore/utils/tf-misses-md.py +10 -4
- package/.tfcore/utils/tf-mockup-parity.mjs +763 -71
- package/.tfcore/utils/tf-mockup-parity.sh +25 -2
- package/.tfcore/utils/tf-owner-text.py +233 -0
- package/.tfcore/utils/tf-owner-text.sh +14 -0
- package/.tfcore/utils/tf-phase.sh +38 -4
- package/.tfcore/utils/tf-portable.sh +230 -0
- package/.tfcore/utils/tf-render-html.py +79 -5
- package/.tfcore/utils/tf-req-ids.py +36 -0
- package/.tfcore/utils/tf-selfcheck.sh +39 -6
- package/.tfcore/utils/tf-split-brd.py +11 -1
- package/.tfcore/utils/tf-stack-check.sh +3 -0
- package/.tfcore/utils/tf-status-evidence.sh +11 -9
- package/.tfcore/utils/tf-status-facts.py +160 -45
- package/.tfcore/utils/tf-theme.mjs +189 -0
- package/.tfcore/utils/tf-triage.py +74 -17
- package/.tfcore/utils/tf-triage.sh +2 -0
- package/.tfcore/utils/tf-verify-boot.sh +529 -54
- package/.tfcore/utils/tf-verify-emit.sh +57 -17
- package/.tfcore/utils/tf-verify-env.sh +33 -4
- package/.tfcore/utils/tf-verify-list.py +290 -24
- package/.tfcore/utils/tf-verify-list.sh +3 -1
- package/.tfcore/utils/tf-verify-native.mjs +307 -0
- package/.tfcore/utils/tf-verify-screens.mjs +341 -44
- package/.tfcore/utils/tf-verify-screens.sh +23 -5
- package/.tfcore/utils/tf-verify-tests.sh +310 -19
- package/.tfcore/utils/tf-verify-verdict.py +163 -21
- package/.tfcore/utils/tf-verify-zoom.mjs +193 -0
- package/.tfcore/utils/tf-verify-zoom.sh +29 -0
- package/.tfcore/utils/tf-yolo-alias.sh +84 -0
- package/.tfcore/utils/tf-yolo-perms.py +135 -0
- package/.tfcore/utils/tf-yolo-start.sh +44 -0
- package/.tfcore/utils/tf-yolo.sh +31 -3
- package/.tfcore/utils/tf_defect.py +27 -0
- package/.tfcore/utils/tf_feedback.py +272 -0
- package/.tfcore/utils/tf_roadmap.py +35 -0
- package/.tfcore/utils/tf_stack_check.py +93 -0
- package/README.md +6 -2
- package/docs/TechieFlow-Installation.md +5 -3
- package/opencode.jsonc +6 -2
- package/package.json +1 -1
- package/scripts/install.mjs +13 -2
- package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-codex-bind.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-codex-telemetry.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
|
@@ -1,31 +1,54 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# tf-verify-boot.sh — start the application for a verify, reach it, and stop it (Sitting 4c, 2026-09-06).
|
|
3
3
|
#
|
|
4
|
-
# bash .tfcore/utils/tf-verify-boot.sh start [--head web|windows|static] [--project <csproj>]
|
|
5
|
-
# [--port N] [--config Release] [--static <dir>]
|
|
6
|
-
#
|
|
7
|
-
# bash .tfcore/utils/tf-verify-boot.sh
|
|
4
|
+
# bash .tfcore/utils/tf-verify-boot.sh start [--head web|windows|static] [--project <csproj>] [--dry-run]
|
|
5
|
+
# [--port N] [--config Release] [--static <dir>] [--probe-path /healthz]
|
|
6
|
+
# [--environment Development] (web: ASPNETCORE_ENVIRONMENT)
|
|
7
|
+
# bash .tfcore/utils/tf-verify-boot.sh stop [--port N]
|
|
8
|
+
# bash .tfcore/utils/tf-verify-boot.sh status [--port N]
|
|
8
9
|
#
|
|
9
10
|
# Heads:
|
|
10
|
-
# web a project on Microsoft.NET.Sdk.Web:
|
|
11
|
-
#
|
|
12
|
-
#
|
|
11
|
+
# web a project on Microsoft.NET.Sdk.Web: published by `tf-build.sh publish` (its rungs, one
|
|
12
|
+
# build at a time) into tests/.artifacts/verify/run-<port>/, and that copy is run on the
|
|
13
|
+
# side that built it, reading its settings from the project folder as `dotnet run` does.
|
|
14
|
+
# Builders side by side never serve each other's half-written build (TF-043). A standalone
|
|
15
|
+
# WebAssembly project is still started with `dotnet run`. Prints BOOTED mode=base url=….
|
|
13
16
|
# windows a MAUI Blazor Hybrid head (UseMaui + a net*-windows target): started Windows-side with
|
|
14
17
|
# WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=9222, the DevTools port
|
|
15
18
|
# relayed to every interface by tf-cdp-relay.ps1, and reached from WSL over CDP. Prints
|
|
16
19
|
# BOOTED mode=cdp url=http://<host>:9223. Proven on MyDiary, 2026-09-06.
|
|
17
20
|
# static a folder of HTML served by python3 (the framework self-test, a mockup set).
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
21
|
+
# maccatalyst a MAUI project with a net*-maccatalyst target, on a Mac only: built by tf-build.sh,
|
|
22
|
+
# its .app started here, and reached over Appium's mac2 driver at the address in
|
|
23
|
+
# core-config.yaml runtimeVerification.appium.maccatalyst.url (default
|
|
24
|
+
# http://localhost:4723; a local Appium that is down is started and later stopped).
|
|
25
|
+
# Prints BOOTED mode=appium url=<appium> bundle=<id>. An SDK that names an older Xcode
|
|
26
|
+
# is built again with -p:ValidateXcodeVersion=false, and the state says so. A Blazor Hybrid
|
|
27
|
+
# head is driven without control names (its data-testid is not readable on a Mac): the
|
|
28
|
+
# state says webview, and those rows say the name check was not measured.
|
|
29
|
+
# BOOTED only once a mac2 session has opened and the app shows a window of its own, not a
|
|
30
|
+
# system dialog; that window is saved as boot-<port>-window.png (Lekhak TF-026). Before the
|
|
31
|
+
# build it checks the two things a fresh macOS 27 / Xcode 27 machine lacks, and names the fix:
|
|
32
|
+
# Automation Mode allowed without a password, and a mac2 driver new enough for this Xcode.
|
|
33
|
+
# `stop` quits the app as Cmd-Q does, and kills it only when it has not quit in 15 s.
|
|
34
|
+
# android, ios: no driver ships in this framework version. NONE with that reason; their rows
|
|
35
|
+
# are recorded as not verified, never as static-only passes.
|
|
36
|
+
# Without --head the script picks web when a web project exists, else (on a Mac) maccatalyst when a
|
|
37
|
+
# MAUI project with that target exists, else windows when a MAUI project with a windows target
|
|
38
|
+
# exists, else NONE. --project names the project when there are several.
|
|
39
|
+
# The state goes to tests/.artifacts/verify/boot-<port>.json and the app log to app-<port>.log, one pair
|
|
40
|
+
# per app, so builders booting side by side never empty or stop each other's (TF-034); boot.json is a
|
|
41
|
+
# copy of the latest start's, which the verdict reads. `stop --port N` stops that app only; a bare
|
|
42
|
+
# `stop` stops the one boot.json names, and refuses while two or more are running. `stop` kills what
|
|
43
|
+
# `start` started and nothing else. Never asks anyone to start anything.
|
|
24
44
|
# Exit 0 booted / stopped · 2 NONE (reason printed; kind=build-error, host or no-driver) · 3 usage.
|
|
25
45
|
set -u
|
|
26
46
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
27
|
-
|
|
47
|
+
source "$HERE/tf-portable.sh" # bash-4 and GNU-only spellings go through the shim (stock Mac)
|
|
48
|
+
DIR="tests/.artifacts/verify"; STATE="$DIR/boot.json"; LOG="$DIR/app.log"; KEYED=""
|
|
28
49
|
mkdir -p "$DIR"
|
|
50
|
+
# once the port is known: this app's own state file and log (TF-034)
|
|
51
|
+
keyed() { KEYED="$DIR/boot-$1.json"; LOG="$DIR/app-$1.log"; : > "$LOG"; }
|
|
29
52
|
|
|
30
53
|
PLATFORM="linux"
|
|
31
54
|
U="$(uname -s 2>/dev/null || echo windows)"
|
|
@@ -39,29 +62,39 @@ winarg() { # a Windows path as a cmd.exe argument: bare when it has no space (a
|
|
|
39
62
|
json_escape() { python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$1"; }
|
|
40
63
|
|
|
41
64
|
write_state() { # head mode url pids rung project reason kind
|
|
42
|
-
python3 - "$@" <<'PY'
|
|
43
|
-
import json, sys, datetime
|
|
65
|
+
TF_BOOT_FILES="$STATE $KEYED" TF_BOOT_LOG="$LOG" python3 - "$@" <<'PY'
|
|
66
|
+
import json, os, sys, datetime
|
|
44
67
|
head, mode, url, pids, rung, project, reason, kind = sys.argv[1:9]
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
68
|
+
s = {"head": head, "mode": mode, "url": url, "pids": [int(p) for p in pids.split() if p.isdigit()],
|
|
69
|
+
"rung": rung, "project": project, "reason": reason, "reason_kind": kind, "stopped": False,
|
|
70
|
+
"platform": sys.argv[9] if len(sys.argv) > 9 else "", "log": os.environ["TF_BOOT_LOG"],
|
|
71
|
+
"started": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")}
|
|
72
|
+
for f in os.environ["TF_BOOT_FILES"].split():
|
|
73
|
+
json.dump(s, open(f, "w"), indent=1)
|
|
50
74
|
PY
|
|
51
75
|
}
|
|
76
|
+
set_state() { # key value-as-JSON: one more field on this start's state files
|
|
77
|
+
TF_BOOT_FILES="$STATE $KEYED" python3 -c 'import json,os,sys
|
|
78
|
+
for p in os.environ["TF_BOOT_FILES"].split():
|
|
79
|
+
s=json.load(open(p)); s[sys.argv[1]]=json.loads(sys.argv[2]); json.dump(s,open(p,"w"),indent=1)' "$1" "$2"
|
|
80
|
+
}
|
|
52
81
|
|
|
53
82
|
find_projects() { # prints csproj paths under src/, source/, the root and one level down; never tests
|
|
54
83
|
find . -maxdepth 4 -name '*.csproj' -not -path '*/bin/*' -not -path '*/obj/*' -not -path '*/node_modules/*' \
|
|
55
84
|
-not -path './tests/*' -not -path '*/test/*' -not -path '*Tests/*' -not -path '*.Tests/*' 2>/dev/null | sed 's#^\./##' | sort
|
|
56
85
|
}
|
|
57
86
|
is_web() { grep -qE 'Microsoft\.NET\.Sdk\.Web|Microsoft\.NET\.Sdk\.BlazorWebAssembly' "$1"; }
|
|
58
|
-
is_maui_win() { grep -qiE '<UseMaui
|
|
87
|
+
is_maui_win() { grep -qiE '<UseMaui>[[:space:]]*true|Microsoft\.NET\.Sdk\.Maui' "$1" && grep -qE 'net[0-9.]+-windows' "$1"; }
|
|
88
|
+
is_maui_mac() { grep -qiE '<UseMaui>[[:space:]]*true|Microsoft\.NET\.Sdk\.Maui' "$1" && grep -qE 'net[0-9.]+-maccatalyst' "$1"; }
|
|
59
89
|
|
|
60
90
|
poll_http() { # url seconds pid-to-watch(optional) -> 0 when it answers
|
|
61
91
|
local url="$1" secs="$2" pid="${3:-}" i=0 code
|
|
62
92
|
while [[ $i -lt $secs ]]; do
|
|
63
93
|
code="$(curl -s -o /dev/null -m 3 -w '%{http_code}' "$url" 2>/dev/null || true)"
|
|
64
|
-
|
|
94
|
+
# Any HTTP answer means the app is serving; this asks whether it is up, not whether the page is
|
|
95
|
+
# right. A Web API with nothing at / answers 404, and was polled for 120 s, called "not brought up"
|
|
96
|
+
# and stopped while its log said "Application started" (AppManager TF-004).
|
|
97
|
+
[[ "$code" =~ ^[1-5][0-9][0-9]$ ]] && return 0
|
|
65
98
|
if [[ -n "$pid" ]] && ! kill -0 "$pid" 2>/dev/null; then return 1; fi
|
|
66
99
|
sleep 2; i=$((i+2))
|
|
67
100
|
done
|
|
@@ -71,13 +104,64 @@ WRONG_RUNG='NETSDK1178|Microsoft\.(iOS|Android|MacCatalyst|tvOS)\.Sdk|Workload I
|
|
|
71
104
|
CODE_ERR='error (CS|RZ|BL|XC|XLS)[0-9]{3,5}|error MSB[0-9]{4}: .*(does not exist|could not be found)|error NU1'
|
|
72
105
|
|
|
73
106
|
cmd="${1:-}"; shift || true
|
|
107
|
+
SPORT=""; [[ "${1:-}" == "--port" && "$cmd" != "start" ]] && SPORT="${2:-}"
|
|
74
108
|
case "$cmd" in
|
|
75
|
-
status)
|
|
109
|
+
status) f="$STATE"; [[ -n "$SPORT" ]] && f="$DIR/boot-$SPORT.json"
|
|
110
|
+
if [[ -f "$f" ]]; then cat "$f"; else echo "NONE no boot state${SPORT:+ for port $SPORT}"; fi; exit 0 ;;
|
|
76
111
|
stop)
|
|
77
|
-
[[ -
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
112
|
+
if [[ -n "$SPORT" ]]; then
|
|
113
|
+
[[ -f "$DIR/boot-$SPORT.json" ]] || { echo "STOPPED nothing was started on port $SPORT"; exit 0; }
|
|
114
|
+
else
|
|
115
|
+
[[ -f "$STATE" ]] || { echo "STOPPED nothing was started"; exit 0; }
|
|
116
|
+
# a bare stop while several apps run could stop another builder's: name the port instead
|
|
117
|
+
live="$(python3 - "$DIR" <<'PY'
|
|
118
|
+
import glob, json, os, re, sys
|
|
119
|
+
out = []
|
|
120
|
+
for f in sorted(glob.glob(os.path.join(sys.argv[1], "boot-*.json"))):
|
|
121
|
+
try:
|
|
122
|
+
s = json.load(open(f))
|
|
123
|
+
except Exception:
|
|
124
|
+
continue
|
|
125
|
+
alive = False
|
|
126
|
+
for p in s.get("pids", []):
|
|
127
|
+
try:
|
|
128
|
+
os.kill(p, 0)
|
|
129
|
+
alive = True
|
|
130
|
+
except Exception:
|
|
131
|
+
pass
|
|
132
|
+
if not s.get("stopped") and alive:
|
|
133
|
+
out.append(re.sub(r"^boot-(.+)[.]json$", r"\1", os.path.basename(f)))
|
|
134
|
+
print(" ".join(out))
|
|
135
|
+
PY
|
|
136
|
+
)"
|
|
137
|
+
if [[ $(wc -w <<<"$live") -ge 2 ]]; then
|
|
138
|
+
echo "NOT-STOPPED $(( $(wc -w <<<"$live") )) apps are running (ports ${live// /, }); stop yours with: bash .tfcore/utils/tf-verify-boot.sh stop --port <n>"; exit 2
|
|
139
|
+
fi
|
|
140
|
+
fi
|
|
141
|
+
python3 - "$DIR" "$SPORT" <<'PY'
|
|
142
|
+
import json, os, re, signal, subprocess, sys, time
|
|
143
|
+
d, port = sys.argv[1], sys.argv[2]
|
|
144
|
+
main = os.path.join(d, "boot.json")
|
|
145
|
+
path = os.path.join(d, f"boot-{port}.json") if port else main
|
|
146
|
+
s = json.load(open(path))
|
|
147
|
+
# A Mac Catalyst head is quit the way Cmd-Q quits it. Killed, macOS offered at the next start to reopen
|
|
148
|
+
# the windows of a run that "quit unexpectedly", and that dialog stood in front of the first screen
|
|
149
|
+
# (Lekhak TF-026). It is killed below only when it has not quit within 15 s.
|
|
150
|
+
if s.get("bundle_id") and s.get("app_path") and not s.get("stopped"):
|
|
151
|
+
exe_dir = os.path.join(s["app_path"], "Contents", "MacOS") + os.sep
|
|
152
|
+
def mac_running():
|
|
153
|
+
out = subprocess.run(["ps", "-A", "-ww", "-o", "command="], capture_output=True).stdout.decode(errors="replace")
|
|
154
|
+
return any(exe_dir in line for line in out.splitlines())
|
|
155
|
+
if mac_running():
|
|
156
|
+
bid = s["bundle_id"].replace('"', "")
|
|
157
|
+
try:
|
|
158
|
+
subprocess.run(["osascript", "-e", f'if application id "{bid}" is running then tell application id "{bid}" to quit'], capture_output=True, timeout=30)
|
|
159
|
+
except Exception:
|
|
160
|
+
pass
|
|
161
|
+
for _ in range(15):
|
|
162
|
+
if not mac_running():
|
|
163
|
+
break
|
|
164
|
+
time.sleep(1)
|
|
81
165
|
for pid in s.get("pids", []):
|
|
82
166
|
for sig in (signal.SIGTERM, signal.SIGKILL):
|
|
83
167
|
try:
|
|
@@ -89,23 +173,88 @@ for pid in s.get("pids", []):
|
|
|
89
173
|
pass
|
|
90
174
|
time.sleep(1)
|
|
91
175
|
image = s.get("win_image") or ""
|
|
92
|
-
if
|
|
93
|
-
|
|
176
|
+
if s.get("win_pids"):
|
|
177
|
+
# this boot's own processes only (Sevak TF-003: /IM stopped every copy, another agent's included)
|
|
178
|
+
for wp in s["win_pids"]:
|
|
179
|
+
subprocess.run(["taskkill.exe", "/F", "/T", "/PID", str(wp)], capture_output=True)
|
|
180
|
+
elif image:
|
|
181
|
+
# a state written before win_pids: by name, as before, unless another start of it is still up
|
|
182
|
+
others = []
|
|
183
|
+
for f in os.listdir(d):
|
|
184
|
+
if f.startswith("boot-") and f.endswith(".json") and os.path.join(d, f) != path:
|
|
185
|
+
try:
|
|
186
|
+
o = json.load(open(os.path.join(d, f)))
|
|
187
|
+
if o.get("win_image") == image and not o.get("stopped") and o.get("pids") != s.get("pids"):
|
|
188
|
+
others.append(f)
|
|
189
|
+
except Exception:
|
|
190
|
+
pass
|
|
191
|
+
if not others:
|
|
192
|
+
subprocess.run(["taskkill.exe", "/F", "/T", "/IM", image], capture_output=True)
|
|
94
193
|
if s.get("win_port"):
|
|
95
194
|
ps = f"Get-NetTCPConnection -LocalPort {s['win_port']} -State Listen -ErrorAction SilentlyContinue | ForEach-Object {{ Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }}"
|
|
96
195
|
subprocess.run(["powershell.exe", "-NoProfile", "-Command", ps], capture_output=True)
|
|
196
|
+
# An app whose starter died is nobody's child any more, so the kills above miss it and it keeps its
|
|
197
|
+
# port and its files. The published copy names it: stop whatever runs from run-<port>/ (TF-043).
|
|
198
|
+
m = re.search(r":(\d+)$", s.get("url") or "")
|
|
199
|
+
run_port = port or (m.group(1) if m else "")
|
|
200
|
+
marks = [os.path.join(os.getcwd(), d, f"run-{run_port}") + os.sep] if run_port else []
|
|
201
|
+
# A Mac Catalyst head: opening an Appium session starts the app again under a new process, so the
|
|
202
|
+
# pid written at boot is gone and the app is not; stop whatever runs from this boot's .app.
|
|
203
|
+
if s.get("app_path"):
|
|
204
|
+
marks.append(os.path.join(s["app_path"], "Contents", "MacOS") + os.sep)
|
|
205
|
+
if marks:
|
|
206
|
+
try:
|
|
207
|
+
procs = [p for p in os.listdir("/proc") if p.isdigit()]
|
|
208
|
+
except Exception:
|
|
209
|
+
procs = []
|
|
210
|
+
cmds = [] # (pid, command line)
|
|
211
|
+
for p in procs:
|
|
212
|
+
try:
|
|
213
|
+
cmd = open(f"/proc/{p}/cmdline", "rb").read().replace(b"\0", b" ").decode(errors="replace")
|
|
214
|
+
except Exception:
|
|
215
|
+
continue
|
|
216
|
+
cmds.append((int(p), cmd))
|
|
217
|
+
if not procs:
|
|
218
|
+
# No /proc (macOS): the same list from ps, full width so the run-<port>/ path is not cut off.
|
|
219
|
+
try:
|
|
220
|
+
out = subprocess.run(["ps", "-A", "-ww", "-o", "pid=,command="], capture_output=True).stdout.decode(errors="replace")
|
|
221
|
+
except Exception:
|
|
222
|
+
out = ""
|
|
223
|
+
for line in out.splitlines():
|
|
224
|
+
pid, _, cmd = line.strip().partition(" ")
|
|
225
|
+
if pid.isdigit():
|
|
226
|
+
cmds.append((int(pid), cmd))
|
|
227
|
+
for pid, cmd in cmds:
|
|
228
|
+
if any(mk in cmd for mk in marks) and pid != os.getpid():
|
|
229
|
+
try:
|
|
230
|
+
os.kill(pid, signal.SIGKILL)
|
|
231
|
+
except Exception:
|
|
232
|
+
pass
|
|
97
233
|
s["stopped"] = True
|
|
98
|
-
json.dump(s, open(
|
|
234
|
+
json.dump(s, open(path, "w"), indent=1)
|
|
235
|
+
# the same app under its other name: boot.json and its own boot-<port>.json
|
|
236
|
+
twins = [main] if port else ([os.path.join(d, f"boot-{m.group(1)}.json")] if m else [])
|
|
237
|
+
for f in twins:
|
|
238
|
+
try:
|
|
239
|
+
o = json.load(open(f))
|
|
240
|
+
if o.get("pids") == s.get("pids"):
|
|
241
|
+
o["stopped"] = True
|
|
242
|
+
json.dump(o, open(f, "w"), indent=1)
|
|
243
|
+
except Exception:
|
|
244
|
+
pass
|
|
99
245
|
print(f"STOPPED head={s.get('head')} mode={s.get('mode')} pids={s.get('pids')}")
|
|
100
246
|
PY
|
|
101
247
|
exit 0 ;;
|
|
102
248
|
start) ;;
|
|
103
|
-
*) sed -n '2,
|
|
249
|
+
*) sed -n '2,39p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 3 ;;
|
|
104
250
|
esac
|
|
105
251
|
|
|
106
|
-
HEAD=""; PROJECT=""; PORT=""; CONFIG=""; STATIC=""
|
|
252
|
+
HEAD=""; PROJECT=""; PORT=""; CONFIG=""; STATIC=""; PROBE="/"; DRYRUN=0; ENVNAME=""
|
|
107
253
|
while [[ $# -gt 0 ]]; do
|
|
108
254
|
case "$1" in
|
|
255
|
+
--environment) ENVNAME="${2:-}"; shift 2 ;; # the web head's ASPNETCORE_ENVIRONMENT (Lekhak TF-001)
|
|
256
|
+
--probe-path) PROBE="${2:-/}"; [[ "$PROBE" == /* ]] || PROBE="/$PROBE"; shift 2 ;;
|
|
257
|
+
--dry-run) DRYRUN=1; shift ;; # print which project and head would boot, and stop (TF-010)
|
|
109
258
|
--head) HEAD="${2:-}"; shift 2 ;;
|
|
110
259
|
--project) PROJECT="${2:-}"; shift 2 ;;
|
|
111
260
|
--port) PORT="${2:-}"; shift 2 ;;
|
|
@@ -114,7 +263,6 @@ while [[ $# -gt 0 ]]; do
|
|
|
114
263
|
*) echo "tf-verify-boot: unknown argument $1" >&2; exit 3 ;;
|
|
115
264
|
esac
|
|
116
265
|
done
|
|
117
|
-
: > "$LOG"
|
|
118
266
|
|
|
119
267
|
# ---- static ---------------------------------------------------------------------------
|
|
120
268
|
if [[ "$HEAD" == "static" ]]; then
|
|
@@ -123,33 +271,72 @@ if [[ "$HEAD" == "static" ]]; then
|
|
|
123
271
|
if curl -s -o /dev/null -m 2 "http://localhost:$PORT/" 2>/dev/null; then
|
|
124
272
|
echo "NONE head=static reason=port $PORT is already in use; stop that process or pass --port"; exit 2
|
|
125
273
|
fi
|
|
274
|
+
keyed "$PORT"
|
|
126
275
|
nohup python3 -m http.server "$PORT" --directory "$STATIC" --bind 127.0.0.1 > "$LOG" 2>&1 &
|
|
127
276
|
PID=$!
|
|
128
277
|
if poll_http "http://localhost:$PORT/" 20 "$PID"; then
|
|
129
278
|
write_state static base "http://localhost:$PORT" "$PID" "python3 http.server" "$STATIC" "" "" "$PLATFORM"
|
|
130
|
-
echo "BOOTED head=static mode=base url=http://localhost:$PORT pid=$PID log=$LOG"; exit 0
|
|
279
|
+
echo "BOOTED head=static mode=base url=http://localhost:$PORT pid=$PID log=$LOG stop=\"bash .tfcore/utils/tf-verify-boot.sh stop --port $PORT\""; exit 0
|
|
131
280
|
fi
|
|
132
281
|
kill "$PID" 2>/dev/null; write_state static none "" "" "" "$STATIC" "http.server did not answer on $PORT" host
|
|
133
282
|
echo "NONE head=static reason=http.server did not answer on $PORT (log $LOG)"; exit 2
|
|
134
283
|
fi
|
|
135
284
|
|
|
136
285
|
# ---- pick the project and the head ------------------------------------------------------
|
|
137
|
-
|
|
138
|
-
WEB=(); WIN=()
|
|
139
|
-
for p in "${ALL[@]}"; do is_web "$p" && WEB+=("$p"); is_maui_win "$p" && WIN+=("$p"); done
|
|
286
|
+
tf_read_lines ALL < <(find_projects)
|
|
287
|
+
WEB=(); WIN=(); MAC=()
|
|
288
|
+
for p in ${ALL[@]+"${ALL[@]}"}; do is_web "$p" && WEB+=("$p"); is_maui_win "$p" && WIN+=("$p"); is_maui_mac "$p" && MAC+=("$p"); done
|
|
289
|
+
# a Mac Catalyst head is picked unasked only on a Mac, the one host that runs it
|
|
290
|
+
[[ "$PLATFORM" == "macos" ]] || MAC=()
|
|
291
|
+
# Several web projects: the one that serves screens. The first in sorted order booted AppManager's
|
|
292
|
+
# external API, which has no screens, in front of its admin site (AppManager TF-010). A project
|
|
293
|
+
# serves screens when its own folder holds .razor or .cshtml pages, or it references a project
|
|
294
|
+
# built with the Razor SDK. One such project is taken and said so; several, or none, stop with the
|
|
295
|
+
# candidates named, because a guess between them grades the wrong app.
|
|
296
|
+
serves_screens() { # csproj -> 0 when it draws pages
|
|
297
|
+
local d; d="$(dirname "$1")"
|
|
298
|
+
find "$d" -maxdepth 3 \( -name '*.razor' -o -name '*.cshtml' \) -not -path '*/bin/*' -not -path '*/obj/*' -print -quit 2>/dev/null | grep -q . && return 0
|
|
299
|
+
local ref
|
|
300
|
+
while read -r ref; do
|
|
301
|
+
ref="$(sed 's#\\#/#g' <<<"$ref")"
|
|
302
|
+
[[ -f "$d/$ref" ]] && grep -qE 'Microsoft\.NET\.Sdk\.Razor' "$d/$ref" && return 0
|
|
303
|
+
done < <(grep -oE '<ProjectReference[^>]*Include="[^"]+"' "$1" 2>/dev/null | sed -E 's/.*Include="([^"]+)".*/\1/')
|
|
304
|
+
return 1
|
|
305
|
+
}
|
|
306
|
+
pick_web() { # sets PROJECT from WEB, or prints NONE and exits
|
|
307
|
+
if [[ ${#WEB[@]} -le 1 ]]; then PROJECT="${WEB[0]:-}"; return; fi
|
|
308
|
+
local ui=()
|
|
309
|
+
for p in "${WEB[@]}"; do serves_screens "$p" && ui+=("$p"); done
|
|
310
|
+
if [[ ${#ui[@]} -eq 1 ]]; then
|
|
311
|
+
PROJECT="${ui[0]}"
|
|
312
|
+
echo "tf-verify-boot: ${#WEB[@]} web projects; $PROJECT serves screens (.razor/.cshtml pages or a Razor SDK reference), the other(s) do not — booting it; --project names another"
|
|
313
|
+
else
|
|
314
|
+
write_state web none "" "" "" "" "${#WEB[@]} web projects and no single one serving screens" host "$PLATFORM"
|
|
315
|
+
echo "NONE head=web reason=${#WEB[@]} web projects (${WEB[*]}) and ${#ui[@]} of them serve screens${ui[*]:+ (${ui[*]})}; name one with --project"; exit 2
|
|
316
|
+
fi
|
|
317
|
+
}
|
|
140
318
|
if [[ -n "$PROJECT" ]]; then
|
|
141
319
|
[[ -f "$PROJECT" ]] || { echo "NONE reason=--project $PROJECT does not exist"; exit 3; }
|
|
142
|
-
if [[ -z "$HEAD" ]]; then
|
|
320
|
+
if [[ -z "$HEAD" ]]; then
|
|
321
|
+
is_web "$PROJECT" && HEAD=web
|
|
322
|
+
[[ -z "$HEAD" && "$PLATFORM" == "macos" ]] && is_maui_mac "$PROJECT" && HEAD=maccatalyst
|
|
323
|
+
[[ -z "$HEAD" ]] && is_maui_win "$PROJECT" && HEAD=windows
|
|
324
|
+
fi
|
|
143
325
|
elif [[ -z "$HEAD" ]]; then
|
|
144
|
-
if [[ ${#WEB[@]} -gt 0 ]]; then HEAD=web;
|
|
326
|
+
if [[ ${#WEB[@]} -gt 0 ]]; then HEAD=web; pick_web
|
|
327
|
+
elif [[ ${#MAC[@]} -gt 0 ]]; then HEAD=maccatalyst; PROJECT="${MAC[0]}"
|
|
145
328
|
elif [[ ${#WIN[@]} -gt 0 ]]; then HEAD=windows; PROJECT="${WIN[0]}"; fi
|
|
146
329
|
else
|
|
147
|
-
case "$HEAD" in web) PROJECT="${
|
|
330
|
+
case "$HEAD" in web) pick_web ;; windows) PROJECT="${WIN[0]:-}" ;; maccatalyst) PROJECT="${MAC[0]:-}" ;; esac
|
|
148
331
|
fi
|
|
332
|
+
if [[ $DRYRUN -eq 1 ]]; then echo "PICK head=${HEAD:-none} project=${PROJECT:-none}"; exit 0; fi
|
|
149
333
|
case "$HEAD" in
|
|
150
|
-
android|ios
|
|
334
|
+
android|ios)
|
|
151
335
|
write_state "$HEAD" none "" "" "" "${PROJECT:-}" "no driver for the $HEAD head ships in this framework version" no-driver "$PLATFORM"
|
|
152
336
|
echo "NONE head=$HEAD reason=no driver for the $HEAD head ships in this framework version; its rows are recorded as not verified"; exit 2 ;;
|
|
337
|
+
maccatalyst)
|
|
338
|
+
[[ "$PLATFORM" == "macos" ]] || { write_state maccatalyst none "" "" "" "${PROJECT:-}" "a Mac Catalyst head runs only on a Mac" host "$PLATFORM"
|
|
339
|
+
echo "NONE head=maccatalyst kind=host reason=a Mac Catalyst head runs only on a Mac, this is $PLATFORM"; exit 2; } ;;
|
|
153
340
|
web|windows) ;;
|
|
154
341
|
"") write_state none none "" "" "" "" "no web project and no MAUI project with a windows target found under src/, source/ or the root" host "$PLATFORM"
|
|
155
342
|
echo "NONE head=none reason=no web project (Microsoft.NET.Sdk.Web) and no MAUI project with a windows target found; name one with --project"; exit 2 ;;
|
|
@@ -160,6 +347,165 @@ PDIR="$(dirname "$PROJECT")"; PNAME="$(basename "$PROJECT" .csproj)"
|
|
|
160
347
|
ASM="$(grep -oE '<AssemblyName>[^<]+' "$PROJECT" 2>/dev/null | head -1 | sed 's/<AssemblyName>//')"; ASM="${ASM:-$PNAME}"
|
|
161
348
|
CFGARGS=(); [[ -n "$CONFIG" ]] && CFGARGS=(-c "$CONFIG")
|
|
162
349
|
|
|
350
|
+
# ---- maccatalyst (MAUI on this Mac, reached over Appium's mac2 driver) -----------------------
|
|
351
|
+
if [[ "$HEAD" == "maccatalyst" ]]; then
|
|
352
|
+
# A Blazor Hybrid head draws its screens in a web view, and on a Mac neither data-testid nor an
|
|
353
|
+
# HTML id reaches mac2 or the macOS accessibility tree (probed 2026-10-07). It is driven all the
|
|
354
|
+
# same, without control names (owner decision A): the state says webview, the screen check skips
|
|
355
|
+
# the name check, and the verdict writes it as not measured.
|
|
356
|
+
WEBVIEW=0; grep -q 'Microsoft\.AspNetCore\.Components\.WebView\.Maui' "$PROJECT" && WEBVIEW=1
|
|
357
|
+
TFM="$(grep -oE 'net[0-9.]+-maccatalyst[0-9.]*' "$PROJECT" | head -1)"
|
|
358
|
+
# the endpoint the app registers (an uncommented maccatalyst: entry with a url), else this Mac
|
|
359
|
+
AURL="$(python3 - <<'PY'
|
|
360
|
+
import re
|
|
361
|
+
try:
|
|
362
|
+
t = open(".tfcore/core-config.yaml", encoding="utf-8").read()
|
|
363
|
+
except Exception:
|
|
364
|
+
t = ""
|
|
365
|
+
m = re.search(r"(?m)^[ \t]+maccatalyst:[ \t]*(?:\{[^}\n]*url:[ \t]*([^,}\s]+)|\n[ \t]+url:[ \t]*(\S+))", t)
|
|
366
|
+
print((m.group(1) or m.group(2)) if m else "")
|
|
367
|
+
PY
|
|
368
|
+
)"
|
|
369
|
+
AURL="${AURL:-http://localhost:4723}"; AURL="${AURL%/}"
|
|
370
|
+
APORT="${AURL##*:}"; [[ "$APORT" =~ ^[0-9]+$ ]] || APORT=4723
|
|
371
|
+
keyed "$APORT"
|
|
372
|
+
mac_none() { # reason: a host fault found before anything was started
|
|
373
|
+
write_state maccatalyst none "" "" "" "$PROJECT" "$1" host "$PLATFORM"
|
|
374
|
+
echo "NONE head=maccatalyst kind=host reason=$1"; exit 2; }
|
|
375
|
+
# Lekhak TF-026: two set-up faults of a fresh macOS 27 / Xcode 27 machine, each of which stopped every
|
|
376
|
+
# mac2 session with an error that does not name its fix. Checked here, before a build of minutes.
|
|
377
|
+
# 1. mac2 turns Automation Mode on for each session; when that needs a password it never comes, and
|
|
378
|
+
# the session fails with "Timed out while enabling automation mode".
|
|
379
|
+
if command -v automationmodetool >/dev/null 2>&1 && automationmodetool 2>&1 | grep -q '^This device REQUIRES user authentication'; then
|
|
380
|
+
mac_none "macOS asks for a password before Automation Mode comes on, so no mac2 session can open. Run once, with an administrator password: sudo automationmodetool enable-automationmode-without-authentication"
|
|
381
|
+
fi
|
|
382
|
+
# 2. mac2 builds its WebDriverAgentMac with the Xcode on this Mac. Before 4.1.1 that build fails
|
|
383
|
+
# under Xcode 27 (deployment target 10.15 below its 12.0 minimum, xcodebuild exit 65). An
|
|
384
|
+
# `appium driver update mac2` left the driver unloadable ("Cannot find package 'appium'");
|
|
385
|
+
# uninstalling and installing again is what worked.
|
|
386
|
+
if command -v appium >/dev/null 2>&1; then
|
|
387
|
+
M2="$(appium driver list --installed --json 2>/dev/null | python3 -c 'import json,sys
|
|
388
|
+
try: print(json.load(sys.stdin).get("mac2", {}).get("version", ""))
|
|
389
|
+
except Exception: print("?")' 2>/dev/null)"
|
|
390
|
+
XMAJ="$(xcodebuild -version 2>/dev/null | sed -n 's/^Xcode \([0-9]*\).*/\1/p' | head -1)"
|
|
391
|
+
REINSTALL="appium driver uninstall mac2; appium driver install mac2"
|
|
392
|
+
if [[ -z "$M2" ]]; then
|
|
393
|
+
mac_none "Appium's mac2 driver is not installed. Run: appium driver install mac2"
|
|
394
|
+
elif [[ "$M2" != "?" && "${XMAJ:-0}" -ge 27 ]] && python3 -c 'import sys
|
|
395
|
+
v = tuple(int(x) for x in sys.argv[1].split("-")[0].split(".")[:3])
|
|
396
|
+
sys.exit(0 if v < (4, 1, 1) else 1)' "$M2"; then
|
|
397
|
+
mac_none "the mac2 driver $M2 cannot build its helper with Xcode $XMAJ (4.1.1 is the first that can). Run: $REINSTALL"
|
|
398
|
+
fi
|
|
399
|
+
fi
|
|
400
|
+
ready() { curl -s -m 3 "$AURL/status" 2>/dev/null | grep -q '"ready":true'; }
|
|
401
|
+
APPIUM_PID=""
|
|
402
|
+
if ! ready; then
|
|
403
|
+
# boot it yourself: a local Appium that is down is started here and stopped by `stop`
|
|
404
|
+
if [[ "$AURL" =~ ^http://(localhost|127\.0\.0\.1): ]] && command -v appium >/dev/null 2>&1; then
|
|
405
|
+
nohup appium --address 127.0.0.1 --port "$APORT" > "$DIR/appium-$APORT.log" 2>&1 < /dev/null &
|
|
406
|
+
APPIUM_PID=$!; i=0
|
|
407
|
+
while [[ $i -lt 60 ]] && ! ready; do sleep 2; i=$((i+2)); done
|
|
408
|
+
fi
|
|
409
|
+
if ! ready; then
|
|
410
|
+
[[ -n "$APPIUM_PID" ]] && kill "$APPIUM_PID" 2>/dev/null
|
|
411
|
+
write_state maccatalyst none "" "" "" "$PROJECT" "Appium did not answer at $AURL/status" host "$PLATFORM"
|
|
412
|
+
echo "NONE head=maccatalyst kind=host reason=Appium did not answer at $AURL/status$([[ -z "$APPIUM_PID" ]] && ! command -v appium >/dev/null 2>&1 && echo '; appium is not installed (docs/TechieFlow-Setup.md §0b step 3)')"; exit 2
|
|
413
|
+
fi
|
|
414
|
+
fi
|
|
415
|
+
stop_appium() { [[ -n "$APPIUM_PID" ]] && kill "$APPIUM_PID" 2>/dev/null; }
|
|
416
|
+
# the Mac rules first (coding-standards-dotnet.md §9): a missing scene manifest is stopped here,
|
|
417
|
+
# before a build that macOS 27 would end at the first window; a WARN is printed and the boot goes on
|
|
418
|
+
chk="$(bash "$HERE/tf-maccatalyst-check.sh" "$PROJECT" 2>&1)"; crc=$?
|
|
419
|
+
printf '%s\n' "$chk" >> "$LOG"; grep -E '^WARN' <<<"$chk" >&2
|
|
420
|
+
OSMAJOR="$(sw_vers -productVersion 2>/dev/null | cut -d. -f1)"
|
|
421
|
+
if [[ $crc -eq 1 && "${OSMAJOR:-0}" -ge 27 ]]; then
|
|
422
|
+
stop_appium; first="$(grep -m1 -E '^FAIL' <<<"$chk" | sed -E 's/^FAIL [^:]+: //')"
|
|
423
|
+
write_state maccatalyst none "" "" "tf-maccatalyst-check.sh" "$PROJECT" "$first" build-error "$PLATFORM"
|
|
424
|
+
echo "NONE head=maccatalyst kind=build-error reason=$first (bash .tfcore/utils/tf-maccatalyst-check.sh)"; exit 2
|
|
425
|
+
fi
|
|
426
|
+
[[ $crc -eq 1 ]] && grep -E '^FAIL' <<<"$chk" | sed 's/^FAIL/WARN (fatal from macOS 27)/' >&2
|
|
427
|
+
BARGS=(-f "$TFM" -c "${CONFIG:-Debug}")
|
|
428
|
+
echo "### build $PROJECT -f $TFM" >> "$LOG"
|
|
429
|
+
out="$(bash "$HERE/tf-build.sh" build "$PROJECT" -- "${BARGS[@]}" 2>&1)"; brc=$?
|
|
430
|
+
# A Mac Catalyst SDK names the one Xcode it was made for and refuses a newer one; tf-build.sh builds
|
|
431
|
+
# again with the check off and says so in a note line (Lekhak TF-032), which the state carries.
|
|
432
|
+
XNOTE="$(grep -oE 'requires Xcode [0-9.]+[.] The current version of Xcode is [0-9.]+' <<<"$(grep '^note' <<<"$out")" | head -1)"
|
|
433
|
+
printf '%s\n' "$out" >> "$LOG"
|
|
434
|
+
verdict="$(grep -E '^(PASS|FAIL|NOT-RUN)' <<<"$out" | tail -1)"
|
|
435
|
+
if [[ $brc -ne 0 ]]; then
|
|
436
|
+
stop_appium
|
|
437
|
+
kind=host; [[ $brc -eq 1 ]] && kind=build-error
|
|
438
|
+
write_state maccatalyst none "" "" "tf-build.sh build -f $TFM" "$PROJECT" "${verdict:-the build failed}" "$kind" "$PLATFORM"
|
|
439
|
+
echo "NONE head=maccatalyst kind=$kind reason=${verdict:-the build failed} (log $LOG)"; exit 2
|
|
440
|
+
fi
|
|
441
|
+
APP="$(ls -td "$PDIR/bin/${CONFIG:-Debug}/$TFM"/*.app "$PDIR/bin/${CONFIG:-Debug}/$TFM"/*/*.app 2>/dev/null | head -1)"
|
|
442
|
+
if [[ -z "$APP" ]]; then
|
|
443
|
+
stop_appium; write_state maccatalyst none "" "" "tf-build.sh build -f $TFM" "$PROJECT" "the build wrote no .app under $PDIR/bin/${CONFIG:-Debug}/$TFM" host "$PLATFORM"
|
|
444
|
+
echo "NONE head=maccatalyst kind=host reason=the build wrote no .app under $PDIR/bin/${CONFIG:-Debug}/$TFM (log $LOG)"; exit 2
|
|
445
|
+
fi
|
|
446
|
+
APP="$(cd "$APP" && pwd)"
|
|
447
|
+
BUNDLE="$(/usr/libexec/PlistBuddy -c 'Print CFBundleIdentifier' "$APP/Contents/Info.plist" 2>/dev/null)"
|
|
448
|
+
EXE="$(/usr/libexec/PlistBuddy -c 'Print CFBundleExecutable' "$APP/Contents/Info.plist" 2>/dev/null)"
|
|
449
|
+
if OLD="$(pgrep -f "$APP/Contents/MacOS/$EXE" | head -1)" && [[ -n "$OLD" ]]; then
|
|
450
|
+
stop_appium; write_state maccatalyst none "" "" "" "$PROJECT" "another copy of $EXE is already running (pid $OLD)" host "$PLATFORM"
|
|
451
|
+
echo "NONE head=maccatalyst kind=host reason=another copy of $EXE is already running (pid $OLD), and mac2 would attach to either; stop it first"; exit 2
|
|
452
|
+
fi
|
|
453
|
+
MARK="$DIR/.launch-$APORT"; : > "$MARK"; ROOT_LOG="$PWD/$LOG"
|
|
454
|
+
# Opened the way Finder opens it: macOS kills a sandboxed app started from its binary directly
|
|
455
|
+
# ("Killed: 9", Lekhak BlogAdmin, 2026-10-07). Its output goes to the log through open's own options.
|
|
456
|
+
# -ApplePersistenceIgnoreState: never offer to reopen the windows of an earlier run (Lekhak TF-026)
|
|
457
|
+
OPENERR="$(open -n -o "$ROOT_LOG" --stderr "$ROOT_LOG" "$APP" --args -ApplePersistenceIgnoreState YES 2>&1)"; printf '%s\n' "$OPENERR" >> "$LOG"
|
|
458
|
+
APP_PID=""; i=0
|
|
459
|
+
while [[ $i -lt 10 && -z "$APP_PID" ]]; do sleep 1; i=$((i+1)); APP_PID="$(pgrep -f "$APP/Contents/MacOS/$EXE" | head -1)"; done
|
|
460
|
+
i=0; while [[ -n "$APP_PID" && $i -lt 8 ]] && kill -0 "$APP_PID" 2>/dev/null; do sleep 1; i=$((i+1)); done
|
|
461
|
+
if [[ -z "$APP_PID" ]] || ! kill -0 "$APP_PID" 2>/dev/null; then
|
|
462
|
+
stop_appium
|
|
463
|
+
crash="$(find "$HOME/Library/Logs/DiagnosticReports" -name "$EXE-*.ips" -newer "$MARK" 2>/dev/null | head -1)"
|
|
464
|
+
hint=""; grep -qs NoSceneLifecycleAdoption "$crash" && hint="; macOS stops a UIKit app that has not adopted scenes: add UIApplicationSceneManifest to Platforms/MacCatalyst/Info.plist and a SceneDelegate : MauiUISceneDelegate"
|
|
465
|
+
# macOS refused to start it at all (Lekhak BlogAdmin, 2026-10-07: "Launch failed", POSIX 163): a
|
|
466
|
+
# locally signed build that asks for keychain-access-groups, which needs a provisioning profile
|
|
467
|
+
if grep -q 'Launch failed' <<<"$OPENERR"; then
|
|
468
|
+
hint="; macOS refused to start it ($(grep -oE 'Code=[0-9]+ "[^"]*"' <<<"$OPENERR" | tail -1))"
|
|
469
|
+
codesign -d --entitlements :- "$APP" 2>/dev/null | grep -q keychain-access-groups && hint="$hint: the build is signed locally (ad hoc) and asks for keychain-access-groups, which needs a provisioning profile; leave that entitlement out of Debug builds or set CodesignProvisioningProfile"
|
|
470
|
+
fi
|
|
471
|
+
write_state maccatalyst none "" "" "tf-build.sh build -f $TFM" "$PROJECT" "the app quit on start${crash:+ (crash report $crash)}$hint" build-error "$PLATFORM"
|
|
472
|
+
echo "NONE head=maccatalyst kind=build-error reason=the app quit on start${crash:+ (crash report $crash)}$hint (log $LOG)"; exit 2
|
|
473
|
+
fi
|
|
474
|
+
write_state maccatalyst appium "$AURL" "$APP_PID $APPIUM_PID" "tf-build.sh build -f $TFM" "$PROJECT" "" "" "$PLATFORM"
|
|
475
|
+
set_state bundle_id "$(json_escape "$BUNDLE")"
|
|
476
|
+
set_state app_path "$(json_escape "$APP")"
|
|
477
|
+
# Lekhak TF-026: a running process and an answering Appium are not a boot. A mac2 session has to open
|
|
478
|
+
# and find the app's own window on view, not a system dialog, before this says BOOTED.
|
|
479
|
+
SHOT="$PWD/$DIR/boot-$APORT-window.png"
|
|
480
|
+
FIRST="$(TF_A="$AURL" TF_B="$BUNDLE" TF_P="$APP" TF_S="$SHOT" node --input-type=module -e "
|
|
481
|
+
import { firstScreen } from '$HERE/tf-appium.mjs';
|
|
482
|
+
const r = await firstScreen(process.env.TF_A, { bundleId: process.env.TF_B, appPath: process.env.TF_P, shot: process.env.TF_S });
|
|
483
|
+
console.log(JSON.stringify(r));" 2>&1 | tail -1)"
|
|
484
|
+
printf '### first screen: %s\n' "$FIRST" >> "$LOG"
|
|
485
|
+
fs() { python3 -c 'import json,sys
|
|
486
|
+
try: print(json.loads(sys.argv[1]).get(sys.argv[2]) or "")
|
|
487
|
+
except Exception: print("")' "$FIRST" "$1"; }
|
|
488
|
+
if [[ "$(fs ok)" != "True" ]]; then
|
|
489
|
+
why="$(fs reason)"; why="${why:-$(cut -c1-200 <<<"$FIRST")}"
|
|
490
|
+
fix=""
|
|
491
|
+
case "$why" in
|
|
492
|
+
*[Aa]utomation\ mode*) fix="; run once, with an administrator password: sudo automationmodetool enable-automationmode-without-authentication" ;;
|
|
493
|
+
*"Cannot find package"*|*"Could not find a driver"*|*xcodebuild*|*"code 65"*) fix="; the mac2 driver does not load or cannot build its helper: appium driver uninstall mac2; appium driver install mac2" ;;
|
|
494
|
+
esac
|
|
495
|
+
[[ "$(fs stage)" == session && -z "$fix" ]] && fix="; a mac2 session that cannot start is usually a permission: System Settings > Privacy & Security > Accessibility (the terminal, and WebDriverAgentRunner-Runner)"
|
|
496
|
+
bash "${BASH_SOURCE[0]}" stop --port "$APORT" >/dev/null 2>&1
|
|
497
|
+
write_state maccatalyst none "" "" "tf-build.sh build -f $TFM" "$PROJECT" "the app started but its first screen was not reached: $why$fix" host "$PLATFORM"
|
|
498
|
+
echo "NONE head=maccatalyst kind=host reason=the app started but its first screen was not reached: $why$fix (log $LOG)"; exit 2
|
|
499
|
+
fi
|
|
500
|
+
set_state first_screen "$(json_escape "$SHOT")"
|
|
501
|
+
DISMISSED="$(fs dismissed)"
|
|
502
|
+
[[ -n "$DISMISSED" ]] && { set_state dismissed "$(json_escape "$DISMISSED")"; echo "tf-verify-boot: $DISMISSED" >&2; }
|
|
503
|
+
[[ -n "$XNOTE" ]] && set_state xcode_check "$(json_escape "off: $XNOTE")"
|
|
504
|
+
[[ $WEBVIEW -eq 1 ]] && set_state webview true
|
|
505
|
+
echo "BOOTED head=maccatalyst mode=appium url=$AURL bundle=$BUNDLE project=$PROJECT tfm=$TFM$([[ $WEBVIEW -eq 1 ]] && echo ' webview=yes (control names not measured)') pid=$APP_PID${APPIUM_PID:+ appium-pid=$APPIUM_PID}${XNOTE:+ xcode-check=off ($XNOTE)} window=$DIR/boot-$APORT-window.png log=$LOG stop=\"bash .tfcore/utils/tf-verify-boot.sh stop --port $APORT\""
|
|
506
|
+
exit 0
|
|
507
|
+
fi
|
|
508
|
+
|
|
163
509
|
# ---- web ------------------------------------------------------------------------------
|
|
164
510
|
if [[ "$HEAD" == "web" ]]; then
|
|
165
511
|
if [[ -z "$PORT" ]]; then
|
|
@@ -171,6 +517,98 @@ if [[ "$HEAD" == "web" ]]; then
|
|
|
171
517
|
echo "NONE head=web reason=port $PORT is already in use; stop that process or pass --port"; exit 2
|
|
172
518
|
fi
|
|
173
519
|
URL="http://localhost:$PORT"
|
|
520
|
+
keyed "$PORT"
|
|
521
|
+
# TF-043. `dotnet run` served the shared bin/ and obj/, which the next builder rewrites under a
|
|
522
|
+
# running app: on TfLens, 2026-09-12, four apps side by side, one sent its stylesheet as 200 with
|
|
523
|
+
# 0 bytes, one answered 500, and one ran a dll older than its own source. Each looked healthy and
|
|
524
|
+
# every measurement taken on it was wrong. So the app runs from a copy of its own, published by
|
|
525
|
+
# tf-build.sh, which lets one build run at a time; settings, secrets and files beside the project
|
|
526
|
+
# are read from the project folder, as `dotnet run` reads them.
|
|
527
|
+
if ! grep -q 'Microsoft\.NET\.Sdk\.BlazorWebAssembly' "$PROJECT"; then
|
|
528
|
+
ROOTDIR="$PWD"; RUN="$DIR/run-$PORT"
|
|
529
|
+
rm -rf "$RUN"
|
|
530
|
+
echo "### publish $PROJECT into $RUN" >> "$LOG"
|
|
531
|
+
# Debug unless --config says otherwise: `dotnet publish` alone builds Release, which is not the app
|
|
532
|
+
# `dotnet run` ran, and a Release publish over a Debug-built tree stamped the pages with one scope
|
|
533
|
+
# and the stylesheet with another (TfLens cluster E, in TF-043)
|
|
534
|
+
pub="$(bash "$HERE/tf-build.sh" publish "$PROJECT" -- -o "$RUN" -c "${CONFIG:-Debug}" 2>&1)"; prc=$?
|
|
535
|
+
printf '%s\n' "$pub" >> "$LOG"
|
|
536
|
+
verdict="$(grep -E '^(PASS|FAIL|NOT-RUN)' <<<"$pub" | tail -1)"
|
|
537
|
+
if [[ $prc -eq 1 ]]; then
|
|
538
|
+
first="$(grep -E 'error (CS|RZ|BL|XC|XLS|MSB|NU)[0-9]+' <<<"$pub" | head -1 | sed 's/^[[:space:]]*//' | cut -c1-160)"
|
|
539
|
+
write_state web none "" "" "tf-build.sh publish" "$PROJECT" "build error: ${first:-$verdict}" build-error "$PLATFORM"
|
|
540
|
+
echo "NONE head=web kind=build-error reason=the code does not build: ${first:-$verdict} (log $LOG)"; exit 2
|
|
541
|
+
elif [[ $prc -ne 0 || ! -f "$RUN/$ASM.dll" ]]; then
|
|
542
|
+
reason="${verdict:-the publish wrote no $ASM.dll into $RUN}"
|
|
543
|
+
write_state web none "" "" "tf-build.sh publish" "$PROJECT" "$reason" host "$PLATFORM"
|
|
544
|
+
echo "NONE head=web kind=host reason=$reason (log $LOG)"; exit 2
|
|
545
|
+
fi
|
|
546
|
+
# what `dotnet run` sets from the launch profile; the environment above all, which picks the
|
|
547
|
+
# settings file and whether the secrets are read
|
|
548
|
+
tf_read_lines LSENV < <(TF_BOOT_ENV="$ENVNAME" python3 - "$PDIR/Properties/launchSettings.json" <<'PY'
|
|
549
|
+
import json, os, sys
|
|
550
|
+
try:
|
|
551
|
+
profiles = json.load(open(sys.argv[1], encoding="utf-8-sig")).get("profiles", {})
|
|
552
|
+
except Exception:
|
|
553
|
+
profiles = {}
|
|
554
|
+
env = next((dict(p.get("environmentVariables") or {}) for p in profiles.values() if p.get("commandName") == "Project"), {})
|
|
555
|
+
env.setdefault("ASPNETCORE_ENVIRONMENT", "Development")
|
|
556
|
+
if os.environ.get("ASPNETCORE_ENVIRONMENT"):
|
|
557
|
+
env["ASPNETCORE_ENVIRONMENT"] = os.environ["ASPNETCORE_ENVIRONMENT"]
|
|
558
|
+
if os.environ.get("TF_BOOT_ENV"):
|
|
559
|
+
env["ASPNETCORE_ENVIRONMENT"] = os.environ["TF_BOOT_ENV"]
|
|
560
|
+
for k, v in env.items():
|
|
561
|
+
print(f"{k}={v}")
|
|
562
|
+
PY
|
|
563
|
+
)
|
|
564
|
+
WIN=""
|
|
565
|
+
case "$verdict" in
|
|
566
|
+
*"via cmd.exe"*|*"via winrun"*|*"via powershell.exe"*)
|
|
567
|
+
WIN=1; WPD="$(winarg "$(wslpath -w "$PDIR")")"; WDLL="$(winarg "$(wslpath -w "$RUN/$ASM.dll")")"
|
|
568
|
+
WWEB=""; [[ -d "$RUN/wwwroot" ]] && WWEB="--webroot $(winarg "$(wslpath -w "$RUN/wwwroot")")"
|
|
569
|
+
sets=""; for kv in ${LSENV[@]+"${LSENV[@]}"}; do sets+="set $kv&& "; done
|
|
570
|
+
nohup cmd.exe /c "cd /d $WPD && ${sets}dotnet $WDLL --urls $URL --contentRoot $WPD $WWEB" >> "$LOG" 2>&1 < /dev/null &
|
|
571
|
+
PID=$!; label="cmd.exe /c dotnet" ;;
|
|
572
|
+
*)
|
|
573
|
+
runner="dotnet"; [[ "$verdict" == *"via ~/.dotnet/dotnet"* ]] && runner="$HOME/.dotnet/dotnet"
|
|
574
|
+
# Lekhak TF-001. A copy run on the WSL side reads user-secrets from ~/.microsoft/usersecrets,
|
|
575
|
+
# while `dotnet user-secrets set` on Windows wrote them to %APPDATA%\Microsoft\UserSecrets:
|
|
576
|
+
# "Required configuration value(s) not set" on an app that starts under `dotnet run`. The
|
|
577
|
+
# secrets reader looks under $APPDATA first on any system, so when only the Windows store
|
|
578
|
+
# holds this project's secrets, the app is pointed at it.
|
|
579
|
+
SID="$(grep -oE '<UserSecretsId>[^<]+' "$PROJECT" 2>/dev/null | head -1 | sed 's/<UserSecretsId>//')"
|
|
580
|
+
if [[ -n "$SID" && ! -f "$HOME/.microsoft/usersecrets/$SID/secrets.json" ]] && [[ "$PLATFORM" == wsl || -n "${TF_WIN_APPDATA:-}" ]]; then
|
|
581
|
+
WAD="${TF_WIN_APPDATA:-}"
|
|
582
|
+
if [[ -z "$WAD" ]] && command -v cmd.exe >/dev/null 2>&1; then
|
|
583
|
+
WAD="$(cmd.exe /c 'echo %APPDATA%' 2>/dev/null | tr -d '\r')"; [[ -n "$WAD" ]] && WAD="$(wslpath -u "$WAD" 2>/dev/null)"
|
|
584
|
+
fi
|
|
585
|
+
if [[ -n "$WAD" && -f "$WAD/Microsoft/UserSecrets/$SID/secrets.json" ]]; then
|
|
586
|
+
LSENV+=("APPDATA=$WAD")
|
|
587
|
+
echo "tf-verify-boot: user-secrets $SID are in the Windows store only; the app reads them from $WAD/Microsoft/UserSecrets"
|
|
588
|
+
fi
|
|
589
|
+
fi
|
|
590
|
+
WEBR=(); [[ -d "$RUN/wwwroot" ]] && WEBR=(--webroot "$ROOTDIR/$RUN/wwwroot")
|
|
591
|
+
# the redirections belong to the whole background group and the group becomes the app: a group
|
|
592
|
+
# left waiting on its app kept this script's output open, so a caller reading it waited for ever
|
|
593
|
+
( cd "$PDIR" || exit 1; exec env ${LSENV[@]+"${LSENV[@]}"} nohup "$runner" "$ROOTDIR/$RUN/$ASM.dll" --urls "$URL" --contentRoot "$PWD" ${WEBR[@]+"${WEBR[@]}"} ) \
|
|
594
|
+
>> "$ROOTDIR/$LOG" 2>&1 < /dev/null &
|
|
595
|
+
PID=$!; label="$([[ "$runner" == dotnet ]] && echo dotnet || echo '~/.dotnet/dotnet')" ;;
|
|
596
|
+
esac
|
|
597
|
+
if poll_http "$URL$PROBE" 120 "$PID"; then
|
|
598
|
+
write_state web base "$URL" "$PID" "$label (published copy)" "$PROJECT" "" "" "$PLATFORM"
|
|
599
|
+
set_state run_dir "$(json_escape "$RUN")"
|
|
600
|
+
[[ -n "$WIN" ]] && set_state win_port "$PORT"
|
|
601
|
+
echo "BOOTED head=web mode=base url=$URL rung=$label project=$PROJECT copy=$RUN pid=$PID log=$LOG stop=\"bash .tfcore/utils/tf-verify-boot.sh stop --port $PORT\""; exit 0
|
|
602
|
+
fi
|
|
603
|
+
pkill -P "$PID" 2>/dev/null; kill "$PID" 2>/dev/null
|
|
604
|
+
[[ -n "$WIN" ]] && powershell.exe -NoProfile -Command "Get-NetTCPConnection -LocalPort $PORT -State Listen -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id \$_.OwningProcess -Force -ErrorAction SilentlyContinue }" >/dev/null 2>&1
|
|
605
|
+
last="$(grep -vE '^[[:space:]]*$' "$LOG" | tail -3 | tr '\n' ' ' | cut -c1-240)"
|
|
606
|
+
# say what the log says: "no rung brought it up" sent a reader to the build when the app had started
|
|
607
|
+
said=""; grep -q 'Now listening on' "$LOG" && said=" although its log says it is listening"
|
|
608
|
+
write_state web none "" "" "$label (published copy)" "$PROJECT" "the published copy did not answer on $URL$PROBE within 120 s$said" host "$PLATFORM"
|
|
609
|
+
echo "NONE head=web kind=host reason=the published copy of $PROJECT did not answer on $URL$PROBE within 120 s$said; last lines: $last (log $LOG)"; exit 2
|
|
610
|
+
fi
|
|
611
|
+
# a standalone WebAssembly project has no server of its own to publish: `dotnet run` serves it
|
|
174
612
|
rungs=()
|
|
175
613
|
case "$PLATFORM" in
|
|
176
614
|
wsl) command -v dotnet >/dev/null 2>&1 && rungs+=("dotnet"); [[ -x "$HOME/.dotnet/dotnet" ]] && rungs+=("$HOME/.dotnet/dotnet"); command -v cmd.exe >/dev/null 2>&1 && rungs+=("cmd.exe") ;;
|
|
@@ -183,23 +621,26 @@ if [[ "$HEAD" == "web" ]]; then
|
|
|
183
621
|
case "$r" in
|
|
184
622
|
cmd.exe)
|
|
185
623
|
WP="$(winarg "$(wslpath -w "$PROJECT")")"
|
|
186
|
-
nohup cmd.exe /c "dotnet run --project $WP ${CFGARGS[*]} --urls $URL" >> "$LOG" 2>&1 < /dev/null &
|
|
624
|
+
nohup cmd.exe /c "dotnet run --project $WP ${CFGARGS[*]:-} --urls $URL" >> "$LOG" 2>&1 < /dev/null &
|
|
187
625
|
PID=$!; label="cmd.exe /c dotnet" ;;
|
|
188
626
|
*)
|
|
189
|
-
ASPNETCORE_ENVIRONMENT="${ASPNETCORE_ENVIRONMENT:-Development}" nohup "$r" run --project "$PROJECT" "${CFGARGS[@]}" --urls "$URL" >> "$LOG" 2>&1 < /dev/null &
|
|
627
|
+
ASPNETCORE_ENVIRONMENT="${ENVNAME:-${ASPNETCORE_ENVIRONMENT:-Development}}" nohup "$r" run --project "$PROJECT" ${CFGARGS[@]+"${CFGARGS[@]}"} --urls "$URL" >> "$LOG" 2>&1 < /dev/null &
|
|
190
628
|
PID=$!; label="$r" ;;
|
|
191
629
|
esac
|
|
192
630
|
tried+=("$label")
|
|
193
|
-
if poll_http "$URL
|
|
631
|
+
if poll_http "$URL$PROBE" 120 "$PID"; then
|
|
194
632
|
WP_PORT=""; [[ "$r" == "cmd.exe" ]] && WP_PORT="$PORT"
|
|
195
633
|
write_state web base "$URL" "$PID" "$label" "$PROJECT" "" "" "$PLATFORM"
|
|
196
|
-
[[ -n "$WP_PORT" ]] &&
|
|
197
|
-
echo "BOOTED head=web mode=base url=$URL rung=$label project=$PROJECT pid=$PID log=$LOG"; exit 0
|
|
634
|
+
[[ -n "$WP_PORT" ]] && set_state win_port "$WP_PORT"
|
|
635
|
+
echo "BOOTED head=web mode=base url=$URL rung=$label project=$PROJECT pid=$PID log=$LOG stop=\"bash .tfcore/utils/tf-verify-boot.sh stop --port $PORT\""; exit 0
|
|
198
636
|
fi
|
|
199
637
|
pkill -P "$PID" 2>/dev/null; kill "$PID" 2>/dev/null
|
|
638
|
+
# killing cmd.exe on this side leaves the Windows-side app running and holding the build output
|
|
639
|
+
# (TF-034): stop whatever listens on THIS start's port, never by program name
|
|
640
|
+
[[ "$r" == "cmd.exe" ]] && powershell.exe -NoProfile -Command "Get-NetTCPConnection -LocalPort $PORT -State Listen -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id \$_.OwningProcess -Force -ErrorAction SilentlyContinue }" >/dev/null 2>&1
|
|
200
641
|
seg="$(awk '/^### rung: /{p=0} index($0,"### rung: '"$r"'")==1{p=1} p' "$LOG")"
|
|
201
642
|
if grep -qE "$CODE_ERR" <<<"$seg" && ! grep -qE "$WRONG_RUNG" <<<"$seg"; then
|
|
202
|
-
first="$(grep -E 'error (CS|RZ|BL|XC|XLS|MSB|NU)[0-9]+' <<<"$seg" | head -1 | sed 's
|
|
643
|
+
first="$(grep -E 'error (CS|RZ|BL|XC|XLS|MSB|NU)[0-9]+' <<<"$seg" | head -1 | sed 's/^[[:space:]]*//' | cut -c1-160)"
|
|
203
644
|
write_state web none "" "" "$label" "$PROJECT" "build error: $first" build-error "$PLATFORM"
|
|
204
645
|
echo "NONE head=web kind=build-error reason=the code does not build via $label: $first (log $LOG)"; exit 2
|
|
205
646
|
fi
|
|
@@ -212,10 +653,41 @@ fi
|
|
|
212
653
|
[[ "$PLATFORM" == "wsl" || "$PLATFORM" == "windows" ]] || { write_state windows none "" "" "" "$PROJECT" "a Windows head runs only from WSL or Windows" host "$PLATFORM"; echo "NONE head=windows reason=a Windows head runs only from WSL or Windows, this is $PLATFORM"; exit 2; }
|
|
213
654
|
command -v cmd.exe >/dev/null 2>&1 || { write_state windows none "" "" "" "$PROJECT" "cmd.exe not reachable" host "$PLATFORM"; echo "NONE head=windows reason=cmd.exe is not reachable from this shell"; exit 2; }
|
|
214
655
|
TFM="$(grep -oE 'net[0-9.]+-windows[0-9.]*' "$PROJECT" | head -1)"
|
|
215
|
-
|
|
656
|
+
RELAY_PORT="${PORT:-9223}"; keyed "$RELAY_PORT"
|
|
657
|
+
# Each boot drives its own embedded browser (Sevak TF-003). The DevTools port was 9222 for every
|
|
658
|
+
# head, so with two up the relay on 9223 listed pages of both apps and a smoke drove the other
|
|
659
|
+
# agent's window. The port now follows the relay port (9223 keeps 9222). A second head while one is
|
|
660
|
+
# up also gets its own WebView2 data folder: two copies of one app would otherwise share one browser
|
|
661
|
+
# process, whose single DevTools port lists both.
|
|
662
|
+
if [[ "$RELAY_PORT" == "9223" ]]; then CDP_PORT=9222; else CDP_PORT=$((RELAY_PORT + 20000)); fi
|
|
663
|
+
OTHERS="$(python3 - "$DIR" "$RELAY_PORT" <<'PY'
|
|
664
|
+
import glob, json, os, sys
|
|
665
|
+
n = 0
|
|
666
|
+
for f in glob.glob(os.path.join(sys.argv[1], "boot-*.json")):
|
|
667
|
+
if f.endswith(f"boot-{sys.argv[2]}.json"):
|
|
668
|
+
continue
|
|
669
|
+
try:
|
|
670
|
+
s = json.load(open(f))
|
|
671
|
+
except Exception:
|
|
672
|
+
continue
|
|
673
|
+
if s.get("head") == "windows" and s.get("mode") == "cdp" and not s.get("stopped"):
|
|
674
|
+
n += 1
|
|
675
|
+
print(n)
|
|
676
|
+
PY
|
|
677
|
+
)"
|
|
678
|
+
UDF_SET=""
|
|
679
|
+
if [[ "${OTHERS:-0}" -gt 0 ]]; then
|
|
680
|
+
UDF="$DIR/webview-$RELAY_PORT"; mkdir -p "$UDF"
|
|
681
|
+
UDF_SET="set WEBVIEW2_USER_DATA_FOLDER=$(wslpath -w "$UDF")&& "
|
|
682
|
+
echo "tf-verify-boot: $OTHERS other Windows head(s) up — this one gets DevTools port $CDP_PORT and its own WebView2 data folder ($UDF), so neither sees the other's pages" >&2
|
|
683
|
+
fi
|
|
216
684
|
WP="$(winarg "$(wslpath -w "$PROJECT")")"; RELAY="$(wslpath -w "$HERE/tf-cdp-relay.ps1")"
|
|
217
|
-
echo "### windows head: $PROJECT -f $TFM" >> "$LOG"
|
|
218
|
-
|
|
685
|
+
echo "### windows head: $PROJECT -f $TFM (DevTools $CDP_PORT, relay $RELAY_PORT)" >> "$LOG"
|
|
686
|
+
# this start's own Windows processes: the app's ids before it, so the new ones are this boot's and
|
|
687
|
+
# stop kills them alone, never every copy of the program (taskkill /IM stopped another agent's app)
|
|
688
|
+
win_ids() { powershell.exe -NoProfile -Command "Get-Process -Name '$ASM' -ErrorAction SilentlyContinue | ForEach-Object { \$_.Id }" 2>/dev/null | tr -d '\r' | grep -E '^[0-9]+$' | sort; }
|
|
689
|
+
BEFORE_IDS="$(win_ids)"
|
|
690
|
+
nohup cmd.exe /c "${UDF_SET}set WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=$CDP_PORT&& dotnet run --project $WP -f $TFM ${CFGARGS[*]:-}" >> "$LOG" 2>&1 < /dev/null &
|
|
219
691
|
APP_PID=$!
|
|
220
692
|
nohup powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$RELAY" -ListenPort "$RELAY_PORT" -TargetPort "$CDP_PORT" >> "$DIR/relay.log" 2>&1 < /dev/null &
|
|
221
693
|
RELAY_PID=$!
|
|
@@ -230,7 +702,7 @@ while [[ $i -lt 300 && -z "$URL" ]]; do
|
|
|
230
702
|
if ! kill -0 "$APP_PID" 2>/dev/null; then
|
|
231
703
|
seg="$(cat "$LOG")"
|
|
232
704
|
if grep -qE "$CODE_ERR" <<<"$seg"; then
|
|
233
|
-
first="$(grep -E 'error (CS|RZ|BL|XC|XLS|MSB|NU)[0-9]+' <<<"$seg" | head -1 | sed 's
|
|
705
|
+
first="$(grep -E 'error (CS|RZ|BL|XC|XLS|MSB|NU)[0-9]+' <<<"$seg" | head -1 | sed 's/^[[:space:]]*//' | cut -c1-160)"
|
|
234
706
|
kill "$RELAY_PID" 2>/dev/null
|
|
235
707
|
write_state windows none "" "" "cmd.exe /c dotnet run -f $TFM" "$PROJECT" "build error: $first" build-error "$PLATFORM"
|
|
236
708
|
echo "NONE head=windows kind=build-error reason=the code does not build: $first (log $LOG)"; exit 2
|
|
@@ -238,13 +710,16 @@ while [[ $i -lt 300 && -z "$URL" ]]; do
|
|
|
238
710
|
fi
|
|
239
711
|
sleep 3; i=$((i+3))
|
|
240
712
|
done
|
|
713
|
+
MINE="$(comm -13 <(printf '%s\n' "$BEFORE_IDS") <(win_ids) | tr '\n' ' ')"
|
|
241
714
|
if [[ -z "$URL" ]]; then
|
|
242
715
|
pkill -P "$APP_PID" 2>/dev/null; kill "$APP_PID" "$RELAY_PID" 2>/dev/null
|
|
243
|
-
taskkill.exe /F /T /
|
|
716
|
+
for w in $MINE; do taskkill.exe /F /T /PID "$w" >/dev/null 2>&1; done
|
|
244
717
|
write_state windows none "" "" "cmd.exe /c dotnet run -f $TFM" "$PROJECT" "the DevTools port $RELAY_PORT never answered within 300 s" host "$PLATFORM"
|
|
245
718
|
echo "NONE head=windows kind=host reason=the app's DevTools port never answered on ${HOSTS[*]}:$RELAY_PORT within 300 s (log $LOG, relay $DIR/relay.log)"; exit 2
|
|
246
719
|
fi
|
|
247
720
|
write_state windows cdp "$URL" "$APP_PID $RELAY_PID" "cmd.exe /c dotnet run -f $TFM" "$PROJECT" "" "" "$PLATFORM"
|
|
248
|
-
|
|
721
|
+
set_state win_image "\"$ASM.exe\""
|
|
722
|
+
set_state win_pids "[$(tr ' ' '\n' <<<"$MINE" | grep -E '^[0-9]+$' | paste -sd, -)]"
|
|
723
|
+
set_state cdp_port "$CDP_PORT"
|
|
249
724
|
echo "BOOTED head=windows mode=cdp url=$URL project=$PROJECT tfm=$TFM pids=$APP_PID,$RELAY_PID image=$ASM.exe log=$LOG"
|
|
250
725
|
exit 0
|