diff --git a/slab/menuband-win/menuband.c b/slab/menuband-win/menuband.c index d898f2b6d7..aa1e34aad8 100644 --- a/slab/menuband-win/menuband.c +++ b/slab/menuband-win/menuband.c @@ -51,6 +51,10 @@ static const GUID MB_SUBTYPE_IEEE_FLOAT = {0x00000003,0x0000,0x0010,{0x80 static void mb_log(const wchar_t *fmt, ...); static void setting_set(const wchar_t *name, DWORD v); +static void unlatch(HWND hwnd, int all); +static void latch_note(int midi); +static int g_enterLatch; +static int g_latched[128]; // Enter-latched notes (drawn lit, sound until unlatched) static const wchar_t *GM_NAMES[128]; static volatile LONG g_dbgNoteOns; @@ -689,7 +693,7 @@ static void paint(void) { RECT kr = white_rect(i); kr.left += dx; kr.right += dx; if (kr.right < g_clipL || kr.left >= g_clipR) continue; RGB8 hi = whiteHi, lo = whiteLo; - int lit = m >= 0 && m < 128 && g_lit[m] > 0; + int lit = m >= 0 && m < 128 && (g_lit[m] > 0 || g_latched[m]); if (lit) { RGB8 l = blend(CHROMA[mv], black, 0.22); hi = l; lo = l; } else if (m == g_hoverMidi && !sliding) { hi = blend(whiteHi, white, 0.12); lo = blend(whiteLo, white, 0.12); } fill(kr, hi, lo, 1.0); @@ -701,7 +705,7 @@ static void paint(void) { RECT br = black_rect(g_blackMidi[i]); br.left += dx; br.right += dx; if (br.right < g_clipL || br.left >= g_clipR) continue; RGB8 hi = blackHi, lo = blackLo; - if (m >= 0 && m < 128 && g_lit[m] > 0) { hi = white; lo = blend(white, blackHi, 0.4); } + if (m >= 0 && m < 128 && (g_lit[m] > 0 || g_latched[m])) { hi = white; lo = blend(white, blackHi, 0.4); } else if (m == g_hoverMidi && !sliding) { hi = blend(blackHi, white, 0.18); lo = blend(blackLo, white, 0.18); } fill(br, hi, lo, 1.0); } @@ -802,6 +806,8 @@ static void build_keymap(void) { #define WM_CTRLTRIPLE (WM_APP + 6) #define WM_PEDAL (WM_APP + 7) #define WM_SURF (WM_APP + 8) +#define WM_LATCH (WM_APP + 9) +#define WM_UNLATCH (WM_APP + 10) static DWORD g_ctrlTapTimes[3]; static int g_ctrlTaps = 0; static int g_ctrlSolo = 0; static LRESULT CALLBACK ll_keyboard(int code, WPARAM wp, LPARAM lp) { @@ -828,6 +834,8 @@ static LRESULT CALLBACK ll_keyboard(int code, WPARAM wp, LPARAM lp) { KBDLLHOOKSTRUCT *k = (KBDLLHOOKSTRUCT *)lp; int down = (wp == WM_KEYDOWN || wp == WM_SYSKEYDOWN); if (k->vkCode == VK_LSHIFT || k->vkCode == VK_RSHIFT || k->vkCode == VK_CAPITAL) { PostMessageW(g_strip, WM_PEDAL, 0, 0); return CallNextHookEx(g_hook, code, wp, lp); } + if (k->vkCode == VK_RETURN) { PostMessageW(g_strip, WM_LATCH, down, 0); return 1; } + if (k->vkCode == VK_BACK) { if (down && !(k->flags & LLKHF_UP)) PostMessageW(g_strip, WM_UNLATCH, (GetAsyncKeyState(VK_SHIFT) & 0x8000) ? 1 : 0, 0); return 1; } if (k->vkCode == VK_LEFT || k->vkCode == VK_RIGHT || k->vkCode == VK_UP || k->vkCode == VK_DOWN) { if (down) PostMessageW(g_strip, WM_SURF, k->vkCode == VK_RIGHT ? 1 : k->vkCode == VK_LEFT ? (WPARAM)-1 : k->vkCode == VK_UP ? 8 : (WPARAM)-8, 0); return 1; @@ -849,6 +857,7 @@ static LRESULT CALLBACK ll_keyboard(int code, WPARAM wp, LPARAM lp) { } static int g_sustained[128]; // note released while its pedal was down; sounds until pedal-up +static int g_latchOrder[128], g_latchCount = 0; // Enter-latch order static int pedal_holds(int midi) { int caps = (GetKeyState(VK_CAPITAL) & 1) != 0; int lshift = (GetAsyncKeyState(VK_LSHIFT) & 0x8000) != 0, rshift = (GetAsyncKeyState(VK_RSHIFT) & 0x8000) != 0; @@ -858,13 +867,27 @@ static int pedal_holds(int midi) { } static void lit_on(HWND hwnd, int midi, int vel) { if (midi < 0 || midi > 127) return; - if (g_sustained[midi]) { g_sustained[midi] = 0; play_off(midi); g_lit[midi] = 0; } // retrigger a pedalled note + if (g_sustained[midi] || g_latched[midi]) { g_sustained[midi] = 0; g_latched[midi] = 0; play_off(midi); g_lit[midi] = 0; } // retrigger a held note if (g_lit[midi]++ == 0) play_on(midi, vel); push_frame(hwnd); } static void lit_off(HWND hwnd, int midi) { if (midi < 0 || midi > 127 || g_lit[midi] == 0) return; - if (--g_lit[midi] == 0) { if (pedal_holds(midi)) g_sustained[midi] = 1; else play_off(midi); } + if (--g_lit[midi] == 0) { if (g_enterLatch) latch_note(midi); else if (g_latched[midi]) { } else if (pedal_holds(midi)) g_sustained[midi] = 1; else play_off(midi); } + push_frame(hwnd); +} +// Enter-latch: while Return is held, a note you release stays on until +// Backspace pops it (newest first) or Shift+Backspace clears them all. +static void latch_note(int midi) { + if (g_latched[midi]) return; + g_latched[midi] = 1; if (g_latchCount < 128) g_latchOrder[g_latchCount++] = midi; +} +static void unlatch(HWND hwnd, int all) { + while (g_latchCount > 0) { + int m = g_latchOrder[--g_latchCount]; + if (g_latched[m]) { g_latched[m] = 0; if (g_lit[m] == 0 && !g_sustained[m]) play_off(m); } + if (!all) break; + } push_frame(hwnd); } static void pedal_release(HWND hwnd) { @@ -896,7 +919,7 @@ static void cue(HWND hwnd, int a, int b) { g_cueNotes[0] = a; g_cueNotes[1] = b; static void typing_set(HWND hwnd, int on) { if (on == g_typing) return; g_typing = on; - if (!on) release_typed(hwnd); + if (!on) { release_typed(hwnd); g_enterLatch = 0; unlatch(hwnd, 1); } g_pendingDigits[0] = 0; flash(on ? FOCUS_BLUE : STOP_RED); cue(hwnd, on ? 84 : 79, on ? 91 : 72); @@ -1224,6 +1247,12 @@ static LRESULT CALLBACK wndproc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { case WM_PEDAL: pedal_release(hwnd); return 0; + case WM_LATCH: + g_enterLatch = (int)wp; + return 0; + case WM_UNLATCH: + unlatch(hwnd, (int)wp); + return 0; case WM_SURF: { // Arrow keys surf the instruments: ←/→ one program, ↑/↓ one family. int next = g_program + (int)(intptr_t)wp; if (next < 0) next += 128; if (next > 127) next -= 128; diff --git a/toolchain/knot/heal-knot.sh b/toolchain/knot/heal-knot.sh new file mode 100755 index 0000000000..6c95e35224 --- /dev/null +++ b/toolchain/knot/heal-knot.sh @@ -0,0 +1,83 @@ +#!/bin/bash +# heal-knot.sh — stop knot.aesthetic.computer wedging every quarter hour. +# +# Diagnosis (2026-10-10): the knot's cgroup is capped at MemoryHigh=1536M, and +# git-upload-pack for the multi-gigabyte core repo runs INSIDE that cgroup +# (a full clone takes ~3 min and gigabytes of pack memory). memory.events +# showed 213,905 "high" throttle events: once a clone is in flight the whole +# cgroup crawls, including `knot keys` which sshd calls for every git auth, +# so pushes hang and port 5555 stops answering. Not a leak — pack memory. +# +# Fix, applied as root over `ssh aesthetic-pds`: +# 1. cap git's pack memory in the bare repos (pack.windowMemory etc.), so a +# clone of core costs a few hundred MB instead of gigabytes; +# 2. loosen the cgroup to what the 3.9 GB box can spare; +# 3. a watchdog timer that restarts knotserver when 5555 stops answering; +# 4. Restart=always so a crash never leaves it down. +# Run from a Mac: bash toolchain/knot/heal-knot.sh +set -euo pipefail +ssh -o BatchMode=yes aesthetic-pds 'bash -s' <<'REMOTE' +set -euo pipefail +echo "== git pack memory caps on the bare repos" +for r in /home/git/repositories/*/ /home/git/repositories/*/*/; do + [ -d "$r" ] || continue + if git -C "$r" rev-parse --is-bare-repository >/dev/null 2>&1; then + git -C "$r" config pack.threads 1 + git -C "$r" config pack.windowMemory 128m + git -C "$r" config pack.deltaCacheSize 64m + git -C "$r" config pack.packSizeLimit 512m + git -C "$r" config core.packedGitWindowSize 32m + git -C "$r" config core.packedGitLimit 256m + git -C "$r" config core.bigFileThreshold 4m + chown git:git "$r/config" 2>/dev/null || true + echo " capped $r" + fi +done +echo "== cgroup: more room, and Restart=always" +mkdir -p /etc/systemd/system/knotserver.service.d +cat > /etc/systemd/system/knotserver.service.d/at-upkeep-memory.conf <<'UNIT' +[Service] +Environment=GOMEMLIMIT=768MiB +MemoryHigh=2600M +MemoryMax=3200M +Restart=always +RestartSec=3 +UNIT +echo "== watchdog: restart when port 5555 stops answering" +cat > /usr/local/bin/knot-watchdog.sh <<'WD' +#!/bin/bash +code=$(curl -s -m 8 -o /dev/null -w "%{http_code}" http://127.0.0.1:5555/ || echo 000) +if [ "$code" = "000" ]; then + logger -t knot-watchdog "port 5555 not answering (rss $(ps -o rss= -C knot | sort -n | tail -1 | tr -d ' ') kB); restarting knotserver" + systemctl restart knotserver.service +fi +WD +chmod +x /usr/local/bin/knot-watchdog.sh +cat > /etc/systemd/system/knot-watchdog.service <<'UNIT' +[Unit] +Description=Restart the knot when it stops answering +[Service] +Type=oneshot +ExecStart=/usr/local/bin/knot-watchdog.sh +UNIT +cat > /etc/systemd/system/knot-watchdog.timer <<'UNIT' +[Unit] +Description=Knot health check every 30 s +[Timer] +OnBootSec=90 +OnUnitActiveSec=30 +AccuracySec=5 +[Install] +WantedBy=timers.target +UNIT +systemctl daemon-reload +systemctl enable --now knot-watchdog.timer +systemctl restart knotserver.service +sleep 6 +echo "== state" +systemctl show knotserver -p MemoryHigh -p MemoryMax -p Restart | tr '\n' ' '; echo +systemctl list-timers knot-watchdog.timer --no-pager | head -3 +curl -s -m 5 -o /dev/null -w "knot http=%{http_code}\n" http://127.0.0.1:5555/ +REMOTE +echo "== from here" +timeout 20 ssh -o BatchMode=yes -T git@knot.aesthetic.computer 2>&1 | head -1