Update configuration files, templates, and internal documentation for the provisioning repository system. Configuration Updates: - KMS configuration modernization - Plugin system settings - Service port mappings - Test cluster topologies - Installation configuration examples - VM configuration defaults - Cedar authorization policies Documentation Updates: - Library module documentation - Extension API guides - AI system documentation - Service management guides - Test environment setup - Plugin usage guides - Validator configuration documentation All changes are backward compatible.
475 lines
24 KiB
HTML
475 lines
24 KiB
HTML
<!DOCTYPE HTML>
|
||
<html lang="en" class="ayu sidebar-visible" dir="ltr">
|
||
<head>
|
||
<!-- Book generated using mdBook -->
|
||
<meta charset="UTF-8">
|
||
<title>Ctrl-C Implementation Notes - Provisioning Platform Documentation</title>
|
||
|
||
|
||
<!-- Custom HTML head -->
|
||
|
||
<meta name="description" content="Complete documentation for the Provisioning Platform - Infrastructure automation with Nushell, KCL, and Rust">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<meta name="theme-color" content="#ffffff">
|
||
|
||
<link rel="icon" href="../favicon.svg">
|
||
<link rel="shortcut icon" href="../favicon.png">
|
||
<link rel="stylesheet" href="../css/variables.css">
|
||
<link rel="stylesheet" href="../css/general.css">
|
||
<link rel="stylesheet" href="../css/chrome.css">
|
||
<link rel="stylesheet" href="../css/print.css" media="print">
|
||
|
||
<!-- Fonts -->
|
||
<link rel="stylesheet" href="../FontAwesome/css/font-awesome.css">
|
||
<link rel="stylesheet" href="../fonts/fonts.css">
|
||
|
||
<!-- Highlight.js Stylesheets -->
|
||
<link rel="stylesheet" id="highlight-css" href="../highlight.css">
|
||
<link rel="stylesheet" id="tomorrow-night-css" href="../tomorrow-night.css">
|
||
<link rel="stylesheet" id="ayu-highlight-css" href="../ayu-highlight.css">
|
||
|
||
<!-- Custom theme stylesheets -->
|
||
|
||
|
||
<!-- Provide site root and default themes to javascript -->
|
||
<script>
|
||
const path_to_root = "../";
|
||
const default_light_theme = "ayu";
|
||
const default_dark_theme = "navy";
|
||
</script>
|
||
<!-- Start loading toc.js asap -->
|
||
<script src="../toc.js"></script>
|
||
</head>
|
||
<body>
|
||
<div id="mdbook-help-container">
|
||
<div id="mdbook-help-popup">
|
||
<h2 class="mdbook-help-title">Keyboard shortcuts</h2>
|
||
<div>
|
||
<p>Press <kbd>←</kbd> or <kbd>→</kbd> to navigate between chapters</p>
|
||
<p>Press <kbd>S</kbd> or <kbd>/</kbd> to search in the book</p>
|
||
<p>Press <kbd>?</kbd> to show this help</p>
|
||
<p>Press <kbd>Esc</kbd> to hide this help</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div id="body-container">
|
||
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||
<script>
|
||
try {
|
||
let theme = localStorage.getItem('mdbook-theme');
|
||
let sidebar = localStorage.getItem('mdbook-sidebar');
|
||
|
||
if (theme.startsWith('"') && theme.endsWith('"')) {
|
||
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
|
||
}
|
||
|
||
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
|
||
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
|
||
}
|
||
} catch (e) { }
|
||
</script>
|
||
|
||
<!-- Set the theme before any content is loaded, prevents flash -->
|
||
<script>
|
||
const default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? default_dark_theme : default_light_theme;
|
||
let theme;
|
||
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||
if (theme === null || theme === undefined) { theme = default_theme; }
|
||
const html = document.documentElement;
|
||
html.classList.remove('ayu')
|
||
html.classList.add(theme);
|
||
html.classList.add("js");
|
||
</script>
|
||
|
||
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
|
||
|
||
<!-- Hide / unhide sidebar before it is displayed -->
|
||
<script>
|
||
let sidebar = null;
|
||
const sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
|
||
if (document.body.clientWidth >= 1080) {
|
||
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
||
sidebar = sidebar || 'visible';
|
||
} else {
|
||
sidebar = 'hidden';
|
||
}
|
||
sidebar_toggle.checked = sidebar === 'visible';
|
||
html.classList.remove('sidebar-visible');
|
||
html.classList.add("sidebar-" + sidebar);
|
||
</script>
|
||
|
||
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||
<!-- populated by js -->
|
||
<mdbook-sidebar-scrollbox class="sidebar-scrollbox"></mdbook-sidebar-scrollbox>
|
||
<noscript>
|
||
<iframe class="sidebar-iframe-outer" src="../toc.html"></iframe>
|
||
</noscript>
|
||
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
|
||
<div class="sidebar-resize-indicator"></div>
|
||
</div>
|
||
</nav>
|
||
|
||
<div id="page-wrapper" class="page-wrapper">
|
||
|
||
<div class="page">
|
||
<div id="menu-bar-hover-placeholder"></div>
|
||
<div id="menu-bar" class="menu-bar sticky">
|
||
<div class="left-buttons">
|
||
<label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||
<i class="fa fa-bars"></i>
|
||
</label>
|
||
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
||
<i class="fa fa-paint-brush"></i>
|
||
</button>
|
||
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||
<li role="none"><button role="menuitem" class="theme" id="default_theme">Auto</button></li>
|
||
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
|
||
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
||
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
||
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
||
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
|
||
</ul>
|
||
<button id="search-toggle" class="icon-button" type="button" title="Search (`/`)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="/ s" aria-controls="searchbar">
|
||
<i class="fa fa-search"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<h1 class="menu-title">Provisioning Platform Documentation</h1>
|
||
|
||
<div class="right-buttons">
|
||
<a href="../print.html" title="Print this book" aria-label="Print this book">
|
||
<i id="print-button" class="fa fa-print"></i>
|
||
</a>
|
||
<a href="https://github.com/provisioning/provisioning-platform" title="Git repository" aria-label="Git repository">
|
||
<i id="git-repository-button" class="fa fa-github"></i>
|
||
</a>
|
||
<a href="https://github.com/provisioning/provisioning-platform/edit/main/provisioning/docs/src/development/CTRL-C_IMPLEMENTATION_NOTES.md" title="Suggest an edit" aria-label="Suggest an edit">
|
||
<i id="git-edit-button" class="fa fa-edit"></i>
|
||
</a>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div id="search-wrapper" class="hidden">
|
||
<form id="searchbar-outer" class="searchbar-outer">
|
||
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||
</form>
|
||
<div id="searchresults-outer" class="searchresults-outer hidden">
|
||
<div id="searchresults-header" class="searchresults-header"></div>
|
||
<ul id="searchresults">
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
||
<script>
|
||
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
||
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
||
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
|
||
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
|
||
});
|
||
</script>
|
||
|
||
<div id="content" class="content">
|
||
<main>
|
||
<h1 id="ctrl-c-handling-implementation-notes"><a class="header" href="#ctrl-c-handling-implementation-notes">CTRL-C Handling Implementation Notes</a></h1>
|
||
<h2 id="overview"><a class="header" href="#overview">Overview</a></h2>
|
||
<p>Implemented graceful CTRL-C handling for sudo password prompts during server creation/generation operations.</p>
|
||
<h2 id="problem-statement"><a class="header" href="#problem-statement">Problem Statement</a></h2>
|
||
<p>When <code>fix_local_hosts: true</code> is set, the provisioning tool requires sudo access to modify <code>/etc/hosts</code> and SSH config. When a user cancels the sudo password prompt (no password, wrong password, timeout), the system would:</p>
|
||
<ol>
|
||
<li>Exit with code 1 (sudo failed)</li>
|
||
<li>Propagate null values up the call stack</li>
|
||
<li>Show cryptic Nushell errors about pipeline failures</li>
|
||
<li>Leave the operation in an inconsistent state</li>
|
||
</ol>
|
||
<p><strong>Important Unix Limitation</strong>: Pressing CTRL-C at the sudo password prompt sends SIGINT to the entire process group, interrupting Nushell before exit code handling can occur. This <strong>cannot be caught</strong> and is expected Unix behavior.</p>
|
||
<h2 id="solution-architecture"><a class="header" href="#solution-architecture">Solution Architecture</a></h2>
|
||
<h3 id="key-principle-return-values-not-exit-codes"><a class="header" href="#key-principle-return-values-not-exit-codes">Key Principle: Return Values, Not Exit Codes</a></h3>
|
||
<p>Instead of using <code>exit 130</code> which kills the entire process, we use <strong>return values</strong> to signal cancellation and let each layer of the call stack handle it gracefully.</p>
|
||
<h3 id="three-layer-approach"><a class="header" href="#three-layer-approach">Three-Layer Approach</a></h3>
|
||
<ol>
|
||
<li>
|
||
<p><strong>Detection Layer</strong> (ssh.nu helper functions)</p>
|
||
<ul>
|
||
<li>Detects sudo cancellation via exit code + stderr</li>
|
||
<li>Returns <code>false</code> instead of calling <code>exit</code></li>
|
||
</ul>
|
||
</li>
|
||
<li>
|
||
<p><strong>Propagation Layer</strong> (ssh.nu core functions)</p>
|
||
<ul>
|
||
<li><code>on_server_ssh()</code>: Returns <code>false</code> on cancellation</li>
|
||
<li><code>server_ssh()</code>: Uses <code>reduce</code> to propagate failures</li>
|
||
</ul>
|
||
</li>
|
||
<li>
|
||
<p><strong>Handling Layer</strong> (create.nu, generate.nu)</p>
|
||
<ul>
|
||
<li>Checks return values</li>
|
||
<li>Displays user-friendly messages</li>
|
||
<li>Returns <code>false</code> to caller</li>
|
||
</ul>
|
||
</li>
|
||
</ol>
|
||
<h2 id="implementation-details"><a class="header" href="#implementation-details">Implementation Details</a></h2>
|
||
<h3 id="1-helper-functions-sshnu11-32"><a class="header" href="#1-helper-functions-sshnu11-32">1. Helper Functions (ssh.nu:11-32)</a></h3>
|
||
<pre><code class="language-nushell">def check_sudo_cached []: nothing -> bool {
|
||
let result = (do --ignore-errors { ^sudo -n true } | complete)
|
||
$result.exit_code == 0
|
||
}
|
||
|
||
def run_sudo_with_interrupt_check [
|
||
command: closure
|
||
operation_name: string
|
||
]: nothing -> bool {
|
||
let result = (do --ignore-errors { do $command } | complete)
|
||
if $result.exit_code == 1 and ($result.stderr | str contains "password is required") {
|
||
print "\n⚠ Operation cancelled - sudo password required but not provided"
|
||
print "ℹ Run 'sudo -v' first to cache credentials, or run without --fix-local-hosts"
|
||
return false # Signal cancellation
|
||
} else if $result.exit_code != 0 and $result.exit_code != 1 {
|
||
error make {msg: $"($operation_name) failed: ($result.stderr)"}
|
||
}
|
||
true
|
||
}
|
||
</code></pre>
|
||
<p><strong>Design Decision</strong>: Return <code>bool</code> instead of throwing error or calling <code>exit</code>. This allows the caller to decide how to handle cancellation.</p>
|
||
<h3 id="2-pre-emptive-warning-sshnu155-160"><a class="header" href="#2-pre-emptive-warning-sshnu155-160">2. Pre-emptive Warning (ssh.nu:155-160)</a></h3>
|
||
<pre><code class="language-nushell">if $server.fix_local_hosts and not (check_sudo_cached) {
|
||
print "\n⚠ Sudo access required for --fix-local-hosts"
|
||
print "ℹ You will be prompted for your password, or press CTRL-C to cancel"
|
||
print " Tip: Run 'sudo -v' beforehand to cache credentials\n"
|
||
}
|
||
</code></pre>
|
||
<p><strong>Design Decision</strong>: Warn users upfront so they’re not surprised by the password prompt.</p>
|
||
<h3 id="3-ctrl-c-detection-sshnu171-199"><a class="header" href="#3-ctrl-c-detection-sshnu171-199">3. CTRL-C Detection (ssh.nu:171-199)</a></h3>
|
||
<p>All sudo commands wrapped with detection:</p>
|
||
<pre><code class="language-nushell">let result = (do --ignore-errors { ^sudo <command> } | complete)
|
||
if $result.exit_code == 1 and ($result.stderr | str contains "password is required") {
|
||
print "\n⚠ Operation cancelled"
|
||
return false
|
||
}
|
||
</code></pre>
|
||
<p><strong>Design Decision</strong>: Use <code>do --ignore-errors</code> + <code>complete</code> to capture both exit code and stderr without throwing exceptions.</p>
|
||
<h3 id="4-state-accumulation-pattern-sshnu122-129"><a class="header" href="#4-state-accumulation-pattern-sshnu122-129">4. State Accumulation Pattern (ssh.nu:122-129)</a></h3>
|
||
<p>Using Nushell’s <code>reduce</code> instead of mutable variables:</p>
|
||
<pre><code class="language-nushell">let all_succeeded = ($settings.data.servers | reduce -f true { |server, acc|
|
||
if $text_match == null or $server.hostname == $text_match {
|
||
let result = (on_server_ssh $settings $server $ip_type $request_from $run)
|
||
$acc and $result
|
||
} else {
|
||
$acc
|
||
}
|
||
})
|
||
</code></pre>
|
||
<p><strong>Design Decision</strong>: Nushell doesn’t allow mutable variable capture in closures. Use <code>reduce</code> for accumulating boolean state across iterations.</p>
|
||
<h3 id="5-caller-handling-createnu262-266-generatenu269-273"><a class="header" href="#5-caller-handling-createnu262-266-generatenu269-273">5. Caller Handling (create.nu:262-266, generate.nu:269-273)</a></h3>
|
||
<pre><code class="language-nushell">let ssh_result = (on_server_ssh $settings $server "pub" "create" false)
|
||
if not $ssh_result {
|
||
_print "\n✗ Server creation cancelled"
|
||
return false
|
||
}
|
||
</code></pre>
|
||
<p><strong>Design Decision</strong>: Check return value and provide context-specific message before returning.</p>
|
||
<h2 id="error-flow-diagram"><a class="header" href="#error-flow-diagram">Error Flow Diagram</a></h2>
|
||
<pre><code>User presses CTRL-C during password prompt
|
||
↓
|
||
sudo exits with code 1, stderr: "password is required"
|
||
↓
|
||
do --ignore-errors captures exit code & stderr
|
||
↓
|
||
Detection logic identifies cancellation
|
||
↓
|
||
Print user-friendly message
|
||
↓
|
||
Return false (not exit!)
|
||
↓
|
||
on_server_ssh returns false
|
||
↓
|
||
Caller (create.nu/generate.nu) checks return value
|
||
↓
|
||
Print "✗ Server creation cancelled"
|
||
↓
|
||
Return false to settings.nu
|
||
↓
|
||
settings.nu handles false gracefully (no append)
|
||
↓
|
||
Clean exit, no cryptic errors
|
||
</code></pre>
|
||
<h2 id="nushell-idioms-used"><a class="header" href="#nushell-idioms-used">Nushell Idioms Used</a></h2>
|
||
<h3 id="1-do---ignore-errors--complete"><a class="header" href="#1-do---ignore-errors--complete">1. <code>do --ignore-errors</code> + <code>complete</code></a></h3>
|
||
<p>Captures both stdout, stderr, and exit code without throwing:</p>
|
||
<pre><code class="language-nushell">let result = (do --ignore-errors { ^sudo command } | complete)
|
||
# result = { stdout: "...", stderr: "...", exit_code: 1 }
|
||
</code></pre>
|
||
<h3 id="2-reduce-for-accumulation"><a class="header" href="#2-reduce-for-accumulation">2. <code>reduce</code> for Accumulation</a></h3>
|
||
<p>Instead of mutable variables in loops:</p>
|
||
<pre><code class="language-nushell"># ❌ BAD - mutable capture in closure
|
||
mut all_succeeded = true
|
||
$servers | each { |s|
|
||
$all_succeeded = false # Error: capture of mutable variable
|
||
}
|
||
|
||
# ✅ GOOD - reduce with accumulator
|
||
let all_succeeded = ($servers | reduce -f true { |s, acc|
|
||
$acc and (check_server $s)
|
||
})
|
||
</code></pre>
|
||
<h3 id="3-early-returns-for-error-handling"><a class="header" href="#3-early-returns-for-error-handling">3. Early Returns for Error Handling</a></h3>
|
||
<pre><code class="language-nushell">if not $condition {
|
||
print "Error message"
|
||
return false
|
||
}
|
||
# Continue with happy path
|
||
</code></pre>
|
||
<h2 id="testing-scenarios"><a class="header" href="#testing-scenarios">Testing Scenarios</a></h2>
|
||
<h3 id="scenario-1-ctrl-c-during-first-sudo-command"><a class="header" href="#scenario-1-ctrl-c-during-first-sudo-command">Scenario 1: CTRL-C During First Sudo Command</a></h3>
|
||
<pre><code class="language-bash">provisioning -c server create
|
||
# Password: [CTRL-C]
|
||
|
||
# Expected Output:
|
||
# ⚠ Operation cancelled - sudo password required but not provided
|
||
# ℹ Run 'sudo -v' first to cache credentials
|
||
# ✗ Server creation cancelled
|
||
</code></pre>
|
||
<h3 id="scenario-2-pre-cached-credentials"><a class="header" href="#scenario-2-pre-cached-credentials">Scenario 2: Pre-cached Credentials</a></h3>
|
||
<pre><code class="language-bash">sudo -v
|
||
provisioning -c server create
|
||
|
||
# Expected: No password prompt, smooth operation
|
||
</code></pre>
|
||
<h3 id="scenario-3-wrong-password-3-times"><a class="header" href="#scenario-3-wrong-password-3-times">Scenario 3: Wrong Password 3 Times</a></h3>
|
||
<pre><code class="language-bash">provisioning -c server create
|
||
# Password: [wrong]
|
||
# Password: [wrong]
|
||
# Password: [wrong]
|
||
|
||
# Expected: Same as CTRL-C (treated as cancellation)
|
||
</code></pre>
|
||
<h3 id="scenario-4-multiple-servers-cancel-on-second"><a class="header" href="#scenario-4-multiple-servers-cancel-on-second">Scenario 4: Multiple Servers, Cancel on Second</a></h3>
|
||
<pre><code class="language-bash"># If creating multiple servers and CTRL-C on second:
|
||
# - First server completes successfully
|
||
# - Second server shows cancellation message
|
||
# - Operation stops, doesn't proceed to third
|
||
</code></pre>
|
||
<h2 id="maintenance-notes"><a class="header" href="#maintenance-notes">Maintenance Notes</a></h2>
|
||
<h3 id="adding-new-sudo-commands"><a class="header" href="#adding-new-sudo-commands">Adding New Sudo Commands</a></h3>
|
||
<p>When adding new sudo commands to the codebase:</p>
|
||
<ol>
|
||
<li>Wrap with <code>do --ignore-errors</code> + <code>complete</code></li>
|
||
<li>Check for exit code 1 + “password is required”</li>
|
||
<li>Return <code>false</code> on cancellation</li>
|
||
<li>Let caller handle the <code>false</code> return value</li>
|
||
</ol>
|
||
<p>Example template:</p>
|
||
<pre><code class="language-nushell">let result = (do --ignore-errors { ^sudo new-command } | complete)
|
||
if $result.exit_code == 1 and ($result.stderr | str contains "password is required") {
|
||
print "\n⚠ Operation cancelled - sudo password required"
|
||
return false
|
||
}
|
||
</code></pre>
|
||
<h3 id="common-pitfalls"><a class="header" href="#common-pitfalls">Common Pitfalls</a></h3>
|
||
<ol>
|
||
<li><strong>Don’t use <code>exit</code></strong>: It kills the entire process</li>
|
||
<li><strong>Don’t use mutable variables in closures</strong>: Use <code>reduce</code> instead</li>
|
||
<li><strong>Don’t ignore return values</strong>: Always check and propagate</li>
|
||
<li><strong>Don’t forget the pre-check warning</strong>: Users should know sudo is needed</li>
|
||
</ol>
|
||
<h2 id="future-improvements"><a class="header" href="#future-improvements">Future Improvements</a></h2>
|
||
<ol>
|
||
<li><strong>Sudo Credential Manager</strong>: Optionally use a credential manager (keychain, etc.)</li>
|
||
<li><strong>Sudo-less Mode</strong>: Alternative implementation that doesn’t require root</li>
|
||
<li><strong>Timeout Handling</strong>: Detect when sudo times out waiting for password</li>
|
||
<li><strong>Multiple Password Attempts</strong>: Distinguish between CTRL-C and wrong password</li>
|
||
</ol>
|
||
<h2 id="references"><a class="header" href="#references">References</a></h2>
|
||
<ul>
|
||
<li>Nushell <code>complete</code> command: https://www.nushell.sh/commands/docs/complete.html</li>
|
||
<li>Nushell <code>reduce</code> command: https://www.nushell.sh/commands/docs/reduce.html</li>
|
||
<li>Sudo exit codes: man sudo (exit code 1 = authentication failure)</li>
|
||
<li>POSIX signal conventions: SIGINT (CTRL-C) = 130</li>
|
||
</ul>
|
||
<h2 id="related-files"><a class="header" href="#related-files">Related Files</a></h2>
|
||
<ul>
|
||
<li><code>provisioning/core/nulib/servers/ssh.nu</code> - Core implementation</li>
|
||
<li><code>provisioning/core/nulib/servers/create.nu</code> - Calls on_server_ssh</li>
|
||
<li><code>provisioning/core/nulib/servers/generate.nu</code> - Calls on_server_ssh</li>
|
||
<li><code>docs/troubleshooting/CTRL-C_SUDO_HANDLING.md</code> - User-facing docs</li>
|
||
<li><code>docs/quick-reference/SUDO_PASSWORD_HANDLING.md</code> - Quick reference</li>
|
||
</ul>
|
||
<h2 id="changelog"><a class="header" href="#changelog">Changelog</a></h2>
|
||
<ul>
|
||
<li><strong>2025-01-XX</strong>: Initial implementation with return values (v2)</li>
|
||
<li><strong>2025-01-XX</strong>: Fixed mutable variable capture with <code>reduce</code> pattern</li>
|
||
<li><strong>2025-01-XX</strong>: First attempt with <code>exit 130</code> (reverted, caused process termination)</li>
|
||
</ul>
|
||
|
||
</main>
|
||
|
||
<nav class="nav-wrapper" aria-label="Page navigation">
|
||
<!-- Mobile navigation buttons -->
|
||
<a rel="prev" href="../development/kcl/VALIDATION_EXECUTIVE_SUMMARY.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||
<i class="fa fa-angle-left"></i>
|
||
</a>
|
||
|
||
<a rel="next prefetch" href="../guides/from-scratch.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||
<i class="fa fa-angle-right"></i>
|
||
</a>
|
||
|
||
<div style="clear: both"></div>
|
||
</nav>
|
||
</div>
|
||
</div>
|
||
|
||
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||
<a rel="prev" href="../development/kcl/VALIDATION_EXECUTIVE_SUMMARY.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||
<i class="fa fa-angle-left"></i>
|
||
</a>
|
||
|
||
<a rel="next prefetch" href="../guides/from-scratch.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||
<i class="fa fa-angle-right"></i>
|
||
</a>
|
||
</nav>
|
||
|
||
</div>
|
||
|
||
<!-- Livereload script (if served using the cli tool) -->
|
||
<script>
|
||
const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||
const wsAddress = wsProtocol + "//" + location.host + "/" + "__livereload";
|
||
const socket = new WebSocket(wsAddress);
|
||
socket.onmessage = function (event) {
|
||
if (event.data === "reload") {
|
||
socket.close();
|
||
location.reload();
|
||
}
|
||
};
|
||
|
||
window.onbeforeunload = function() {
|
||
socket.close();
|
||
}
|
||
</script>
|
||
|
||
|
||
|
||
<script>
|
||
window.playground_copyable = true;
|
||
</script>
|
||
|
||
|
||
<script src="../elasticlunr.min.js"></script>
|
||
<script src="../mark.min.js"></script>
|
||
<script src="../searcher.js"></script>
|
||
|
||
<script src="../clipboard.min.js"></script>
|
||
<script src="../highlight.js"></script>
|
||
<script src="../book.js"></script>
|
||
|
||
<!-- Custom JS scripts -->
|
||
|
||
|
||
</div>
|
||
</body>
|
||
</html>
|