<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>State-Machine on The Nested Lab</title>
    <link>https://thenestedlab.com/tags/state-machine/</link>
    <description>Recent content in State-Machine on The Nested Lab</description>
    <generator>Hugo</generator>
    <language>en-gb</language>
    <lastBuildDate>Thu, 17 Sep 2026 06:20:00 +0100</lastBuildDate>
    <atom:link href="https://thenestedlab.com/tags/state-machine/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Windows Server 2025 via Aria Automation, part 2: a state machine that survives four reboots</title>
      <link>https://thenestedlab.com/posts/windows-2025-aria-part-2/</link>
      <pubDate>Thu, 17 Sep 2026 06:20:00 +0100</pubDate>
      <guid>https://thenestedlab.com/posts/windows-2025-aria-part-2/</guid>
      <description>05-build-master.ps1 runs at every startup until the build is done. Flag files make each step run once; a persisted state file keeps true timings across reboots; a kill-switch flag makes a stray run a no-op. Then it cleans up, validates, publishes — and deletes itself.</description>
      <content:encoded><![CDATA[<p><a href="/posts/windows-2025-aria-part-1/">Part 1</a> ended with a startup scheduled
task registered and <code>05-build-master.ps1</code> staged locally. From this point
the machine will reboot at least twice more — Windows Updates insists, and
the build ends with a clean reboot — and every one of those boots runs the
same script. The script has to <em>converge</em> on a finished build no matter how
many times it&rsquo;s invoked.</p>
<p>That&rsquo;s a state machine, and it&rsquo;s built from three very boring mechanisms.</p>
<h2 id="the-three-mechanisms">The three mechanisms</h2>
<p><strong>1. Master kill switch.</strong> If <code>100_build_complete.flag</code> exists, exit
immediately. A stray task run after completion does nothing.</p>
<p><strong>2. Per-step flags.</strong> Every install step writes
<code>NN_&lt;Step&gt;_installed.flag</code> on success and is skipped on subsequent runs.</p>
<p><strong>3. Persisted state.</strong> <code>data-state.json</code> holds the provisioning start
time and per-step timings, re-loaded on every boot. So the final report
shows the <em>true</em> duration of every step across the whole build, not just
the last boot — and a step re-observed as SKIPPED after a reboot never
overwrites its real recorded duration.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl"><span class="n">every</span> <span class="n">boot</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="mi">100</span><span class="n">_build_complete</span><span class="o">.</span><span class="n">flag</span> <span class="err">→</span> <span class="n">exit</span>
</span></span><span class="line"><span class="cl">  <span class="nb">load</span> <span class="n">data</span><span class="o">-</span><span class="n">state</span><span class="o">.</span><span class="n">json</span>
</span></span><span class="line"><span class="cl">  <span class="k">for</span> <span class="n">step</span> <span class="ow">in</span> <span class="o">$</span><span class="n">softwarePayload</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="n">NN_step_installed</span><span class="o">.</span><span class="n">flag</span> <span class="err">→</span> <span class="n">SKIP</span> <span class="p">(</span><span class="n">keep</span> <span class="n">recorded</span> <span class="n">timing</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">run</span> <span class="n">installer</span> <span class="n">from</span> <span class="n">local</span> <span class="n">Staging</span> <span class="p">(</span><span class="n">timeout</span> <span class="n">ceiling</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">exit</span> <span class="mi">0</span> <span class="ow">or</span> <span class="mi">3010</span> <span class="err">→</span> <span class="n">write</span> <span class="n">flag</span><span class="p">,</span> <span class="n">record</span> <span class="n">timing</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="n">step</span><span class="o">.</span><span class="n">RebootAfter</span> <span class="err">→</span> <span class="n">Restart</span><span class="o">-</span><span class="n">Computer</span><span class="p">;</span> <span class="n">exit</span>      <span class="err">←</span> <span class="n">next</span> <span class="n">boot</span> <span class="n">resumes</span> <span class="n">here</span>
</span></span><span class="line"><span class="cl">  <span class="n">cleanup</span> <span class="err">→</span> <span class="n">validate</span> <span class="err">→</span> <span class="n">publish</span> <span class="err">→</span> <span class="bp">self</span><span class="o">-</span><span class="n">destruct</span> <span class="err">→</span> <span class="n">final</span> <span class="n">reboot</span>
</span></span></code></pre></div><h2 id="phase-1-software-declaratively">Phase 1: software, declaratively</h2>
<p>The stack is a single array — the designed extension point:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nv">$softwarePayload</span> <span class="p">=</span> <span class="vm">@</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="vm">@</span><span class="p">{</span> <span class="n">Name</span><span class="p">=</span><span class="s1">&#39;MonitoringAgent&#39;</span><span class="p">;</span> <span class="n">Folder</span><span class="p">=</span><span class="nv">$p</span><span class="p">.</span><span class="n">monPath</span><span class="p">;</span>  <span class="n">File</span><span class="p">=</span><span class="nv">$p</span><span class="p">.</span><span class="n">monInstallFile</span><span class="p">;</span>  <span class="n">Timeout</span><span class="p">=</span><span class="mf">20</span><span class="p">;</span> <span class="n">Reboot</span><span class="p">=</span><span class="vm">$false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">     <span class="n">Service</span><span class="p">=</span><span class="s1">&#39;HealthService&#39;</span><span class="p">;</span>       <span class="n">Path</span><span class="p">=</span><span class="s1">&#39;...\Monitoring Agent\HealthService.exe&#39;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="vm">@</span><span class="p">{</span> <span class="n">Name</span><span class="p">=</span><span class="s1">&#39;InventoryAgent&#39;</span><span class="p">;</span>  <span class="n">Folder</span><span class="p">=</span><span class="nv">$p</span><span class="p">.</span><span class="n">invPath</span><span class="p">;</span>  <span class="n">File</span><span class="p">=</span><span class="nv">$p</span><span class="p">.</span><span class="n">invInstallFile</span><span class="p">;</span>  <span class="n">Timeout</span><span class="p">=</span><span class="mf">15</span><span class="p">;</span> <span class="n">Reboot</span><span class="p">=</span><span class="vm">$false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">     <span class="n">Service</span><span class="p">=</span><span class="s1">&#39;InventoryAgent&#39;</span><span class="p">;</span>      <span class="n">Path</span><span class="p">=</span><span class="s1">&#39;...\Inventory\agent.exe&#39;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="vm">@</span><span class="p">{</span> <span class="n">Name</span><span class="p">=</span><span class="s1">&#39;EndpointSecurity&#39;</span><span class="p">;</span><span class="n">Folder</span><span class="p">=</span><span class="nv">$p</span><span class="p">.</span><span class="n">epsPath</span><span class="p">;</span>  <span class="n">File</span><span class="p">=</span><span class="nv">$p</span><span class="p">.</span><span class="n">epsInstallFile</span><span class="p">;</span>  <span class="n">Timeout</span><span class="p">=</span><span class="mf">30</span><span class="p">;</span> <span class="n">Reboot</span><span class="p">=</span><span class="vm">$false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">     <span class="n">Service</span><span class="p">=</span><span class="s1">&#39;masvc&#39;</span><span class="p">;</span>               <span class="n">Path</span><span class="p">=</span><span class="s1">&#39;...\Agent\masvc.exe&#39;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="vm">@</span><span class="p">{</span> <span class="n">Name</span><span class="p">=</span><span class="s1">&#39;LogAgent&#39;</span><span class="p">;</span>        <span class="n">Folder</span><span class="p">=</span><span class="nv">$p</span><span class="p">.</span><span class="n">logPath</span><span class="p">;</span>  <span class="n">File</span><span class="p">=</span><span class="nv">$p</span><span class="p">.</span><span class="n">logInstallFile</span><span class="p">;</span>  <span class="n">Timeout</span><span class="p">=</span><span class="mf">15</span><span class="p">;</span> <span class="n">Reboot</span><span class="p">=</span><span class="vm">$false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">     <span class="n">Service</span><span class="p">=</span><span class="s1">&#39;LogAgentService&#39;</span><span class="p">;</span>     <span class="n">Path</span><span class="p">=</span><span class="s1">&#39;...\Log Agent\liwinsvc.exe&#39;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="vm">@</span><span class="p">{</span> <span class="n">Name</span><span class="p">=</span><span class="s1">&#39;WindowsUpdates&#39;</span><span class="p">;</span>  <span class="n">Folder</span><span class="p">=</span><span class="nv">$p</span><span class="p">.</span><span class="n">updPath</span><span class="p">;</span>  <span class="n">File</span><span class="p">=</span><span class="nv">$p</span><span class="p">.</span><span class="n">updInstallFile</span><span class="p">;</span>  <span class="n">Timeout</span><span class="p">=</span><span class="mf">45</span><span class="p">;</span> <span class="n">Reboot</span><span class="p">=</span><span class="vm">$true</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></div><p>Adding a product is adding an element. Each installer runs from the local
staging copy with a hung-installer timeout; exit codes 0 and <strong>3010</strong>
(success, reboot required) count as success. When a <code>Reboot=$true</code> step
succeeds, the script restarts the machine and exits; on the next boot the
task re-runs it, completed steps skip via their flags, execution resumes at
the next step.</p>
<p>Post-install health isn&rsquo;t &ldquo;installer said OK&rdquo; — it&rsquo;s <strong>flag present AND
service running AND install path exists</strong>, with a 120-second wait for
delayed-start services. Windows Updates is flag-only; there&rsquo;s no service
to check.</p>
<h2 id="phase-2-cleanup--leave-nothing-behind">Phase 2: cleanup — leave nothing behind</h2>
<ul>
<li><strong>Eject the config-drive ISO.</strong> In Session 0 there&rsquo;s no Explorer, so
the usual shell ejection doesn&rsquo;t work. A P/Invoke to <code>winmm</code>&rsquo;s
<code>mciSendString(&quot;set cdaudio door open&quot;)</code> does.</li>
<li><strong>Restore UAC.</strong> <code>EnableLUA</code> and <code>FilterAdministratorToken</code> were relaxed
in the base image so the build could run unattended; re-enable both.</li>
<li><strong>Uninstall cloudbase-init.</strong> Stop and delete the service, kill its
processes, run the uninstaller silently, remove the directory. Guarded
by <code>99_cleanup_complete.flag</code>.</li>
</ul>
<p>The startup task is deleted <em>before</em> validation, so the health check can
truthfully report &ldquo;no automation task remains&rdquo;.</p>
<h2 id="phase-3-collect-the-evidence">Phase 3: collect the evidence</h2>
<p>The script assembles <code>data-validation.json</code> — the single input to the
report in <a href="/posts/windows-2025-aria-part-3/">part 3</a>:</p>
<table>
	<thead>
			<tr>
					<th>Collector</th>
					<th>Captures</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Network</td>
					<td>per active adapter: alias, IPv4, prefix, gateway, MAC, DNS</td>
			</tr>
			<tr>
					<td>Infrastructure</td>
					<td><code>guestinfo.vra.infrastructure</code> via <code>vmtoolsd</code>, <strong>15 attempts × 10 s</strong> (the vRO subscription can land late)</td>
			</tr>
			<tr>
					<td>OS / hardware</td>
					<td>domain, CPUs, RAM, caption, uptime, time zone, pending reboot, latest hotfix</td>
			</tr>
			<tr>
					<td>AD / security</td>
					<td>local Administrators, secure-channel test, machine OU</td>
			</tr>
			<tr>
					<td>Disks</td>
					<td>per volume: letter, label, size, free</td>
			</tr>
			<tr>
					<td>Post-build health</td>
					<td>WinRM, RDP, UAC enabled, startup task gone, domain DNS resolves, NTP source</td>
			</tr>
			<tr>
					<td>Software</td>
					<td>per app: flag + service + path</td>
			</tr>
			<tr>
					<td>Installed apps</td>
					<td>both uninstall hives (64-bit and WOW6432)</td>
			</tr>
	</tbody>
</table>
<p>The verdict is strict: <strong><code>GuestStatus = Success</code> only if every software
item is healthy <em>and</em> the machine is domain-joined.</strong> Anything else renders
the report banner red.</p>
<h2 id="phases-4-and-5-publish-then-self-destruct">Phases 4 and 5: publish, then self-destruct</h2>
<p>Remap the share with the <strong>write</strong> account (5 × 10 s retries — a
different account from the read-only one used for pulls; a compromised
build guest can&rsquo;t tamper with the engine). Render the HTML report locally.
Write <code>100_build_complete.flag</code> — kill switch armed. Copy the report plus
<code>Data\</code>, <code>Flags\</code>, <code>Logs\</code> to <code>\\share\Builds\&lt;image&gt;\&lt;HOSTNAME&gt;\</code>. Stop the
transcript <em>before</em> copying logs so the final log is complete and unlocked.</p>
<p>Then: delete the startup task; overwrite both share passwords in the
on-disk payload with <code>*** SCRUBBED ***</code>; delete <code>Data\</code> and <code>Staging\</code>
(and <code>Flags\</code>/<code>Logs\</code> if the share copy succeeded); delete the sibling
scripts and itself; remove <code>Scripts\</code>; reboot one final time.</p>
<p>The delivered server boots clean, domain-joined, agents running, and
carries <strong>no credentials, payloads or tooling</strong>.</p>
<h2 id="the-reboot-sequence-of-a-nominal-build">The reboot sequence of a nominal build</h2>
<ol>
<li>After static networking (<code>00</code>, exit 1003)</li>
<li>After engine pull (<code>03</code>, exit 1003) — ends the cloudbase-init phase</li>
<li>After Windows Updates (<code>05</code>, <code>Reboot=$true</code>)</li>
<li>Final, after publish and self-destruct</li>
</ol>
<p>The report&rsquo;s timeline chart finds the update reboot automatically: any gap
over 30 seconds between steps is shaded and labelled REBOOT.</p>
<p>At completion the <code>Flags\</code> folder is the whole history of the build in
file names:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl"><span class="n">Flags</span>  <span class="mi">00</span><span class="n">_config</span><span class="o">-</span><span class="n">network</span><span class="o">.</span><span class="n">flag</span>
</span></span><span class="line"><span class="cl">  <span class="mi">01</span><span class="n">_config</span><span class="o">-</span><span class="n">disks</span><span class="o">.</span><span class="n">flag</span>
</span></span><span class="line"><span class="cl">  <span class="mi">02</span><span class="n">_join</span><span class="o">-</span><span class="n">domain</span><span class="o">.</span><span class="n">flag</span>
</span></span><span class="line"><span class="cl">  <span class="mi">03</span><span class="n">_init</span><span class="o">-</span><span class="n">puller</span><span class="o">.</span><span class="n">flag</span>
</span></span><span class="line"><span class="cl">  <span class="mi">04</span><span class="n">_stage</span><span class="o">-</span><span class="n">payloads</span><span class="o">.</span><span class="n">flag</span>
</span></span><span class="line"><span class="cl">  <span class="mi">01</span><span class="n">_MonitoringAgent_installed</span><span class="o">.</span><span class="n">flag</span>
</span></span><span class="line"><span class="cl">  <span class="mi">02</span><span class="n">_InventoryAgent_installed</span><span class="o">.</span><span class="n">flag</span>
</span></span><span class="line"><span class="cl">  <span class="mi">03</span><span class="n">_EndpointSecurity_installed</span><span class="o">.</span><span class="n">flag</span>
</span></span><span class="line"><span class="cl">  <span class="mi">04</span><span class="n">_LogAgent_installed</span><span class="o">.</span><span class="n">flag</span>
</span></span><span class="line"><span class="cl">  <span class="mi">05</span><span class="n">_WindowsUpdates_installed</span><span class="o">.</span><span class="n">flag</span>
</span></span><span class="line"><span class="cl">  <span class="mi">99</span><span class="n">_cleanup_complete</span><span class="o">.</span><span class="n">flag</span>
</span></span><span class="line"><span class="cl">  <span class="mi">100</span><span class="n">_build_complete</span><span class="o">.</span><span class="n">flag</span>        <span class="o">&lt;-</span> <span class="n">kill</span> <span class="k">switch</span>
</span></span></code></pre></div><p>And the persisted timings turn into this, in the report from <a href="/posts/windows-2025-aria-part-3/">part
3</a>:</p>
<p><img alt="Provisioning timeline from the build report: five install steps, a shaded four-minute REBOOT gap after Windows Updates, then cleanup, collection and write steps" loading="lazy" src="/images/ui/a4-aria-report-timeline.jpg">
<em>Every bar sits at its true wall-clock position across the reboots because the state file carried the timings. The amber band is the reboot after Windows Updates, found automatically from the gap.</em></p>
<h2 id="why-this-matters-outside-the-lab">Why this matters outside the lab</h2>
<p>What customers get from a reboot-safe build is predictability: every
server takes the same steps in the same order, survives the reboots
Windows insists on, and finishes clean — with no tooling, no credentials
and no leftover tasks on the delivered machine. That last part matters to
security reviewers as much as the first part matters to operations. And
because every step&rsquo;s timing is recorded, &ldquo;why did this build take twice as
long?&rdquo; has an answer instead of a guess.</p>
<h2 id="rules-learned">Rules learned</h2>
<ul>
<li>A reboot-safe build is <strong>kill switch + per-step flags + persisted
timings</strong>. Nothing cleverer is needed.</li>
<li>Treat exit <strong>3010</strong> as success. Let the <em>step</em> declare whether to
reboot; the loop handles it.</li>
<li>Health = flag <strong>and</strong> service <strong>and</strong> path. Installer exit codes lie.</li>
<li>Read platform-injected metadata with <strong>retries</strong> — the injecting
workflow may land after the guest starts looking.</li>
<li>Two share accounts: read-only for pulls, write-only for publishing.</li>
<li>Delete the task before validating, so &ldquo;no task remains&rdquo; is checkable.</li>
<li>Stop the transcript before you copy the logs.</li>
<li>Scrub, delete yourself, reboot. The customer gets a server, not a
build environment.</li>
</ul>
<p><em>Part 3: <a href="/posts/windows-2025-aria-part-3/">validation as a product, and the details that hurt</a>.</em></p>
<hr>
<p><em>Lab write-up of a production pattern; customer specifics removed. Opinions
my own.</em></p>
]]></content:encoded>
    </item>
  </channel>
</rss>
