<?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>Emulator on Tarragon</title><link>https://tarrragon.github.io/blog/tags/emulator/</link><description>Recent content in Emulator on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Tue, 16 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/emulator/index.xml" rel="self" type="application/rss+xml"/><item><title>Firestore Hands-on 操作路線</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/hands-on/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/hands-on/</guid><description>&lt;p>Firestore hands-on 操作路線的核心責任是把 deep article 的機制判讀轉成可在本地演練的操作。這一層全程跑在 &lt;a href="https://firebase.google.com/docs/emulator-suite">Firebase Emulator Suite&lt;/a> 上——本地、免費、不碰雲端專案、不產生計費，讓讀者能建立資料、寫規則測試、跑分片計數，並取得 query output、測試結果與 artifact，而不只停在概念。&lt;/p>
&lt;h2 id="為什麼用-emulator">為什麼用 emulator&lt;/h2>
&lt;p>Firestore 的 client 直連模型讓「在本地驗證」變得重要：規則寫錯是資安漏洞、查詢設計錯是成本事故，這些都該在進雲端前用真實求值引擎驗過。Emulator Suite 提供與雲端一致的 Firestore 行為與 Security Rules 求值引擎，是規則測試的官方推薦環境。要留意的邊界是——emulator 模擬功能行為，但不模擬計費與部分 production 規模限制（單 document 寫入軟上限、連線天花板）。涉及成本與規模的判讀仍以雲端為準，emulator lab 會在對應處標明。&lt;/p>
&lt;h2 id="章節列表">章節列表&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>章節&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;th>產出 artifact&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="local-emulator-quickstart/">Local emulator quickstart&lt;/a>&lt;/td>
 &lt;td>emulator 啟動、&lt;code>firestore.rules&lt;/code>、admin seed、query baseline&lt;/td>
 &lt;td>emulator config、seed script、query output&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="security-rules-test-lab/">Security Rules test lab&lt;/a>&lt;/td>
 &lt;td>&lt;code>@firebase/rules-unit-testing&lt;/code>、放行 / 拒絕斷言、CI 整合&lt;/td>
 &lt;td>rules 測試檔、pass / fail 結果、emulators:exec log&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="distributed-counter-lab/">Distributed counter lab&lt;/a>&lt;/td>
 &lt;td>分片計數寫入、shard 分佈、讀取彙總、contention 的 production 邊界&lt;/td>
 &lt;td>counter script、shard 分佈 output、彙總驗證&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="設計原則">設計原則&lt;/h2>
&lt;p>Firestore hands-on 章節以「進雲端前先驗」為中心。操作指令只在能產出 artifact 時出現；每篇都要回答 emulator 在哪裡跑、需要哪些 input、怎麼知道操作成功（query output / 測試斷言 / shard 分佈），以及哪些 production 特性（計費、寫入上限）emulator 不負責、要回雲端確認。&lt;/p>
&lt;h2 id="引用路徑">引用路徑&lt;/h2>
&lt;ul>
&lt;li>上游：&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/" data-link-title="Firestore" data-link-desc="Firebase / Google Cloud 的 serverless document database、collection / document 模型、client 直連 &amp;#43; Security Rules、realtime listener 與 offline 同步、BaaS bundle 的資料層面">Firestore overview&lt;/a>&lt;/li>
&lt;li>Deep article：&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/security-rules-authz-modeling/" data-link-title="Firestore Security Rules 授權建模與可測試化：把規則當程式碼治理" data-link-desc="Firestore client 直連模型把整個授權控制面壓在 Security Rules 這套 DSL 裡；本文展開規則的求值模型、把授權拆成可組合 function、用 emulator 寫單元測試、五個把規則寫成資安漏洞的 production 踩坑，以及規則複雜度撞牆時把授權拉回後端的邊界">Security Rules 授權建模&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/distributed-counter-high-frequency-write/" data-link-title="Firestore 高頻寫入與 distributed counter：單 document contention 邊界與分片計數" data-link-desc="Firestore 單一 document 有持續寫入的軟上限、高頻計數寫爆 contention 是常見事故；本文展開寫入 contention 的成因、distributed counter 分片計數的實作與讀取彙總、shard 數量與讀寫成本的取捨、五個高頻寫入踩坑，以及計數需求超過分片能處理時改走外部聚合的邊界">distributed counter 高頻寫入&lt;/a>&lt;/li>
&lt;li>發布證據：&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate&lt;/a>（規則測試接進 gate）&lt;/li>
&lt;li>官方：&lt;a href="https://firebase.google.com/docs/emulator-suite">Emulator Suite&lt;/a>、&lt;a href="https://firebase.google.com/docs/emulator-suite/connect_firestore">Connect to Firestore emulator&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Firestore hands-on 操作路線的核心責任是把 deep article 的機制判讀轉成可在本地演練的操作。這一層全程跑在 <a href="https://firebase.google.com/docs/emulator-suite">Firebase Emulator Suite</a> 上——本地、免費、不碰雲端專案、不產生計費，讓讀者能建立資料、寫規則測試、跑分片計數，並取得 query output、測試結果與 artifact，而不只停在概念。</p>
<h2 id="為什麼用-emulator">為什麼用 emulator</h2>
<p>Firestore 的 client 直連模型讓「在本地驗證」變得重要：規則寫錯是資安漏洞、查詢設計錯是成本事故，這些都該在進雲端前用真實求值引擎驗過。Emulator Suite 提供與雲端一致的 Firestore 行為與 Security Rules 求值引擎，是規則測試的官方推薦環境。要留意的邊界是——emulator 模擬功能行為，但不模擬計費與部分 production 規模限制（單 document 寫入軟上限、連線天花板）。涉及成本與規模的判讀仍以雲端為準，emulator lab 會在對應處標明。</p>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>產出 artifact</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="local-emulator-quickstart/">Local emulator quickstart</a></td>
          <td>emulator 啟動、<code>firestore.rules</code>、admin seed、query baseline</td>
          <td>emulator config、seed script、query output</td>
      </tr>
      <tr>
          <td><a href="security-rules-test-lab/">Security Rules test lab</a></td>
          <td><code>@firebase/rules-unit-testing</code>、放行 / 拒絕斷言、CI 整合</td>
          <td>rules 測試檔、pass / fail 結果、emulators:exec log</td>
      </tr>
      <tr>
          <td><a href="distributed-counter-lab/">Distributed counter lab</a></td>
          <td>分片計數寫入、shard 分佈、讀取彙總、contention 的 production 邊界</td>
          <td>counter script、shard 分佈 output、彙總驗證</td>
      </tr>
  </tbody>
</table>
<h2 id="設計原則">設計原則</h2>
<p>Firestore hands-on 章節以「進雲端前先驗」為中心。操作指令只在能產出 artifact 時出現；每篇都要回答 emulator 在哪裡跑、需要哪些 input、怎麼知道操作成功（query output / 測試斷言 / shard 分佈），以及哪些 production 特性（計費、寫入上限）emulator 不負責、要回雲端確認。</p>
<h2 id="引用路徑">引用路徑</h2>
<ul>
<li>上游：<a href="/blog/backend/01-database/vendors/firestore/" data-link-title="Firestore" data-link-desc="Firebase / Google Cloud 的 serverless document database、collection / document 模型、client 直連 &#43; Security Rules、realtime listener 與 offline 同步、BaaS bundle 的資料層面">Firestore overview</a></li>
<li>Deep article：<a href="/blog/backend/01-database/vendors/firestore/security-rules-authz-modeling/" data-link-title="Firestore Security Rules 授權建模與可測試化：把規則當程式碼治理" data-link-desc="Firestore client 直連模型把整個授權控制面壓在 Security Rules 這套 DSL 裡；本文展開規則的求值模型、把授權拆成可組合 function、用 emulator 寫單元測試、五個把規則寫成資安漏洞的 production 踩坑，以及規則複雜度撞牆時把授權拉回後端的邊界">Security Rules 授權建模</a> / <a href="/blog/backend/01-database/vendors/firestore/distributed-counter-high-frequency-write/" data-link-title="Firestore 高頻寫入與 distributed counter：單 document contention 邊界與分片計數" data-link-desc="Firestore 單一 document 有持續寫入的軟上限、高頻計數寫爆 contention 是常見事故；本文展開寫入 contention 的成因、distributed counter 分片計數的實作與讀取彙總、shard 數量與讀寫成本的取捨、五個高頻寫入踩坑，以及計數需求超過分片能處理時改走外部聚合的邊界">distributed counter 高頻寫入</a></li>
<li>發布證據：<a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a>（規則測試接進 gate）</li>
<li>官方：<a href="https://firebase.google.com/docs/emulator-suite">Emulator Suite</a>、<a href="https://firebase.google.com/docs/emulator-suite/connect_firestore">Connect to Firestore emulator</a></li>
</ul>
]]></content:encoded></item><item><title>Firestore Local Emulator Quickstart</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/hands-on/local-emulator-quickstart/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/hands-on/local-emulator-quickstart/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/hands-on/" data-link-title="Firestore Hands-on 操作路線" data-link-desc="用 Firebase Emulator Suite 在本地演練 Firestore：emulator quickstart、Security Rules 單元測試、distributed counter 分片計數，全程零雲端成本、可重跑、產出可驗證 artifact">Firestore Hands-on 操作路線&lt;/a> 的基礎 lab。指令以 &lt;a href="https://firebase.google.com/docs/cli">Firebase CLI 文件&lt;/a> 與 &lt;a href="https://firebase.google.com/docs/emulator-suite">Emulator Suite 文件&lt;/a> 為準、最後檢查日 2026-06-16。&lt;/p>&lt;/blockquote>
&lt;p>Firestore local emulator quickstart 的核心責任是建立後續 Security Rules 測試與 distributed counter lab 共用的本地環境。這個 lab 把 Firestore 從抽象服務轉成可觀察的 emulator、規則檔、seed 資料與 query 結果，全程不碰雲端專案。&lt;/p>
&lt;p>本文的驗收標準是：你能在本地啟動 Firestore emulator、用 admin SDK 寫入並查詢一組 seed 資料、看到 emulator UI 裡的資料，並知道 cleanup 路徑。&lt;/p>
&lt;h2 id="lab-環境與前置">Lab 環境與前置&lt;/h2>
&lt;p>Lab 在本地資料夾跑，需要 Node.js 與 Firebase CLI。以下命令建立一個可刪除的工作區並裝好工具。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">mkdir -p /tmp/firestore-lab
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nb">cd&lt;/span> /tmp/firestore-lab
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># Firebase CLI（已裝可跳過）；用 npx 也可避免全域安裝&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">npm install -g firebase-tools
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1"># 本 lab 的 Node 依賴&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">npm init -y
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">npm install firebase-admin&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>emulator 需要 Java runtime（Firestore emulator 跑在 JVM 上）。&lt;code>java -version&lt;/code> 確認存在；缺的話先裝 JDK 再繼續。驗收 artifact 是 &lt;code>/tmp/firestore-lab&lt;/code> 工作區。&lt;/p>
&lt;h2 id="emulator-設定">Emulator 設定&lt;/h2>
&lt;p>&lt;code>firebase.json&lt;/code> 的核心責任是宣告要啟動哪些 emulator 與對應 port。這裡只開 Firestore 與 UI，不需要真實 Firebase 專案——emulator 用一個 demo project id 即可，&lt;code>demo-&lt;/code> 前綴讓 CLI 知道這是純本地、不連雲端。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">cat &amp;gt; firebase.json &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;JSON&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="s">{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="s"> &amp;#34;emulators&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="s"> &amp;#34;firestore&amp;#34;: { &amp;#34;port&amp;#34;: 8080 },
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="s"> &amp;#34;ui&amp;#34;: { &amp;#34;enabled&amp;#34;: true, &amp;#34;port&amp;#34;: 4000 }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="s"> },
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="s"> &amp;#34;firestore&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="s"> &amp;#34;rules&amp;#34;: &amp;#34;firestore.rules&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="s">JSON&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="baseline-規則">Baseline 規則&lt;/h2>
&lt;p>&lt;code>firestore.rules&lt;/code> 的核心責任是定義授權。Quickstart 先用一組明確的 owner-scoped 規則（不是 &lt;code>allow read, write: if true&lt;/code>，那是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/security-rules-authz-modeling/" data-link-title="Firestore Security Rules 授權建模與可測試化：把規則當程式碼治理" data-link-desc="Firestore client 直連模型把整個授權控制面壓在 Security Rules 這套 DSL 裡；本文展開規則的求值模型、把授權拆成可組合 function、用 emulator 寫單元測試、五個把規則寫成資安漏洞的 production 踩坑，以及規則複雜度撞牆時把授權拉回後端的邊界">deep article Case 1&lt;/a> 的漏洞）。這份規則後續在 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/firestore/hands-on/security-rules-test-lab/" data-link-title="Firestore Security Rules Test Lab" data-link-desc="用 @firebase/rules-unit-testing 在 emulator 上把 Security Rules 寫成自動化測試：放行 / 越權拒絕 / 未登入拒絕 / 欄位竄改拒絕四類斷言、firebase emulators:exec 在 CI 跑、把規則測試接進 release gate">Security Rules test lab&lt;/a> 會被測試覆蓋。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/firestore/hands-on/" data-link-title="Firestore Hands-on 操作路線" data-link-desc="用 Firebase Emulator Suite 在本地演練 Firestore：emulator quickstart、Security Rules 單元測試、distributed counter 分片計數，全程零雲端成本、可重跑、產出可驗證 artifact">Firestore Hands-on 操作路線</a> 的基礎 lab。指令以 <a href="https://firebase.google.com/docs/cli">Firebase CLI 文件</a> 與 <a href="https://firebase.google.com/docs/emulator-suite">Emulator Suite 文件</a> 為準、最後檢查日 2026-06-16。</p></blockquote>
<p>Firestore local emulator quickstart 的核心責任是建立後續 Security Rules 測試與 distributed counter lab 共用的本地環境。這個 lab 把 Firestore 從抽象服務轉成可觀察的 emulator、規則檔、seed 資料與 query 結果，全程不碰雲端專案。</p>
<p>本文的驗收標準是：你能在本地啟動 Firestore emulator、用 admin SDK 寫入並查詢一組 seed 資料、看到 emulator UI 裡的資料，並知道 cleanup 路徑。</p>
<h2 id="lab-環境與前置">Lab 環境與前置</h2>
<p>Lab 在本地資料夾跑，需要 Node.js 與 Firebase CLI。以下命令建立一個可刪除的工作區並裝好工具。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">mkdir -p /tmp/firestore-lab
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">cd</span> /tmp/firestore-lab
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># Firebase CLI（已裝可跳過）；用 npx 也可避免全域安裝</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">npm install -g firebase-tools
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># 本 lab 的 Node 依賴</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">npm init -y
</span></span><span class="line"><span class="ln">9</span><span class="cl">npm install firebase-admin</span></span></code></pre></div><p>emulator 需要 Java runtime（Firestore emulator 跑在 JVM 上）。<code>java -version</code> 確認存在；缺的話先裝 JDK 再繼續。驗收 artifact 是 <code>/tmp/firestore-lab</code> 工作區。</p>
<h2 id="emulator-設定">Emulator 設定</h2>
<p><code>firebase.json</code> 的核心責任是宣告要啟動哪些 emulator 與對應 port。這裡只開 Firestore 與 UI，不需要真實 Firebase 專案——emulator 用一個 demo project id 即可，<code>demo-</code> 前綴讓 CLI 知道這是純本地、不連雲端。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl">cat &gt; firebase.json <span class="s">&lt;&lt;&#39;JSON&#39;
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="s">{
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s">  &#34;emulators&#34;: {
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s">    &#34;firestore&#34;: { &#34;port&#34;: 8080 },
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s">    &#34;ui&#34;: { &#34;enabled&#34;: true, &#34;port&#34;: 4000 }
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">  },
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">  &#34;firestore&#34;: {
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">    &#34;rules&#34;: &#34;firestore.rules&#34;
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s">  }
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s">}
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s">JSON</span></span></span></code></pre></div><h2 id="baseline-規則">Baseline 規則</h2>
<p><code>firestore.rules</code> 的核心責任是定義授權。Quickstart 先用一組明確的 owner-scoped 規則（不是 <code>allow read, write: if true</code>，那是 <a href="/blog/backend/01-database/vendors/firestore/security-rules-authz-modeling/" data-link-title="Firestore Security Rules 授權建模與可測試化：把規則當程式碼治理" data-link-desc="Firestore client 直連模型把整個授權控制面壓在 Security Rules 這套 DSL 裡；本文展開規則的求值模型、把授權拆成可組合 function、用 emulator 寫單元測試、五個把規則寫成資安漏洞的 production 踩坑，以及規則複雜度撞牆時把授權拉回後端的邊界">deep article Case 1</a> 的漏洞）。這份規則後續在 <a href="/blog/backend/01-database/vendors/firestore/hands-on/security-rules-test-lab/" data-link-title="Firestore Security Rules Test Lab" data-link-desc="用 @firebase/rules-unit-testing 在 emulator 上把 Security Rules 寫成自動化測試：放行 / 越權拒絕 / 未登入拒絕 / 欄位竄改拒絕四類斷言、firebase emulators:exec 在 CI 跑、把規則測試接進 release gate">Security Rules test lab</a> 會被測試覆蓋。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl">cat &gt; firestore.rules <span class="s">&lt;&lt;&#39;RULES&#39;
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="s">rules_version = &#39;2&#39;;
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s">service cloud.firestore {
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s">  match /databases/{database}/documents {
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s">    match /notes/{noteId} {
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">      allow read: if request.auth != null
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">                  &amp;&amp; resource.data.ownerId == request.auth.uid;
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">      allow create: if request.auth != null
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s">                    &amp;&amp; request.resource.data.ownerId == request.auth.uid;
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s">      allow update, delete: if request.auth != null
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s">                            &amp;&amp; resource.data.ownerId == request.auth.uid;
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="s">    }
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="s">  }
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="s">}
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="s">RULES</span></span></span></code></pre></div><h2 id="啟動-emulator">啟動 emulator</h2>
<p>啟動 emulator 的核心責任是讓本地有一個可寫可查的 Firestore。用 demo project id 啟動，emulator UI 在 <code>http://localhost:4000</code> 可看到資料。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">firebase emulators:start --only firestore --project demo-firestore-lab</span></span></code></pre></div><p>這個指令會 foreground 跑住 emulator。保持它開著，另開一個 terminal 做 seed 與 query。終端輸出會印出 Firestore emulator 的位址（預設 <code>localhost:8080</code>）與 UI 位址。</p>
<h2 id="seed-資料admin-sdk-繞過規則">Seed 資料（admin SDK 繞過規則）</h2>
<p>Seed 的核心責任是建立可重跑的測試資料。admin SDK 連到 emulator 時繞過 Security Rules（模擬後端的特權寫入），適合種資料。關鍵是設 <code>FIRESTORE_EMULATOR_HOST</code> 環境變數——有了它，admin SDK 的寫入全部導向 emulator、不需要任何雲端 credential。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl">cat &gt; seed.js <span class="s">&lt;&lt;&#39;JS&#39;
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="s">const admin = require(&#39;firebase-admin&#39;);
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s">admin.initializeApp({ projectId: &#39;demo-firestore-lab&#39; });
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s">const db = admin.firestore();
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">async function main() {
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">  await db.collection(&#39;notes&#39;).doc(&#39;n1&#39;).set({
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">    ownerId: &#39;alice&#39;, text: &#39;Alice first note&#39;, createdAt: Date.now(),
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s">  });
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s">  await db.collection(&#39;notes&#39;).doc(&#39;n2&#39;).set({
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s">    ownerId: &#39;bob&#39;, text: &#39;Bob first note&#39;, createdAt: Date.now(),
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="s">  });
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="s">  console.log(&#39;seeded 2 notes&#39;);
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="s">}
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="s">main().then(() =&gt; process.exit(0));
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="s">JS</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"># 在新 terminal、同 lab 目錄下</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="nb">export</span> <span class="nv">FIRESTORE_EMULATOR_HOST</span><span class="o">=</span>localhost:8080
</span></span><span class="line"><span class="ln">20</span><span class="cl">node seed.js</span></span></code></pre></div><p>預期輸出 <code>seeded 2 notes</code>。打開 <code>http://localhost:4000/firestore</code> 應看到 <code>notes</code> collection 下兩筆 document。</p>
<h2 id="query-baseline">Query baseline</h2>
<p>Query 的核心責任是確認資料可讀、access pattern 入口可用。admin SDK 同樣繞過規則，這裡驗證的是資料與查詢本身（規則的放行 / 拒絕在下一個 lab 用 client context 驗）。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl">cat &gt; query.js <span class="s">&lt;&lt;&#39;JS&#39;
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="s">const admin = require(&#39;firebase-admin&#39;);
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s">admin.initializeApp({ projectId: &#39;demo-firestore-lab&#39; });
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s">const db = admin.firestore();
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s">async function main() {
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s">  const snap = await db.collection(&#39;notes&#39;)
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s">    .where(&#39;ownerId&#39;, &#39;==&#39;, &#39;alice&#39;).get();
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s">  console.log(`alice notes: ${snap.size}`);
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s">  snap.forEach((d) =&gt; console.log(d.id, d.data().text));
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s">}
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="s">main().then(() =&gt; process.exit(0));
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="s">JS</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="nb">export</span> <span class="nv">FIRESTORE_EMULATOR_HOST</span><span class="o">=</span>localhost:8080
</span></span><span class="line"><span class="ln">16</span><span class="cl">node query.js</span></span></code></pre></div><p>預期輸出 <code>alice notes: 1</code> 與 <code>n1 Alice first note</code>。這證明 <code>where('ownerId', '==', ...)</code> 的 access pattern 成立——它也正是 client 端要自帶、好讓 owner-scoped 規則放行的查詢條件。</p>
<h2 id="artifact-與驗收">Artifact 與驗收</h2>
<table>
  <thead>
      <tr>
          <th>Artifact</th>
          <th>路徑 / 來源</th>
          <th>驗收</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>emulator config</td>
          <td><code>firebase.json</code></td>
          <td>Firestore + UI port 宣告</td>
      </tr>
      <tr>
          <td>規則檔</td>
          <td><code>firestore.rules</code></td>
          <td>owner-scoped、非 <code>if true</code></td>
      </tr>
      <tr>
          <td>seed 結果</td>
          <td><code>seed.js</code> output + UI</td>
          <td><code>notes/n1</code>、<code>notes/n2</code> 存在</td>
      </tr>
      <tr>
          <td>query 結果</td>
          <td><code>query.js</code> output</td>
          <td><code>alice notes: 1</code></td>
      </tr>
  </tbody>
</table>
<h2 id="cleanup">Cleanup</h2>
<p>Cleanup 的核心責任是讓 lab 可重跑。emulator 的資料在 process 結束時預設不持久化（除非設了 <code>--export-on-exit</code>），所以停掉 emulator 等於清空資料。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 停掉 emulator：在 emulator terminal 按 Ctrl-C</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 移除整個工作區</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">rm -rf /tmp/firestore-lab</span></span></code></pre></div><p>若想保留 emulator 資料跨 session，啟動時加 <code>--import=./data --export-on-exit=./data</code>；lab 預設不持久化，保持每次乾淨起步。</p>
<p>完成本篇後，下一步進 <a href="/blog/backend/01-database/vendors/firestore/hands-on/security-rules-test-lab/" data-link-title="Firestore Security Rules Test Lab" data-link-desc="用 @firebase/rules-unit-testing 在 emulator 上把 Security Rules 寫成自動化測試：放行 / 越權拒絕 / 未登入拒絕 / 欄位竄改拒絕四類斷言、firebase emulators:exec 在 CI 跑、把規則測試接進 release gate">Security Rules test lab</a>（把上面的規則寫成自動化測試）或 <a href="/blog/backend/01-database/vendors/firestore/hands-on/distributed-counter-lab/" data-link-title="Firestore Distributed Counter Lab" data-link-desc="在 emulator 上實作 distributed counter：建立 N 個 shard、隨機分片寫入、觀察 shard 分佈是否均勻、讀取彙總驗證總和正確，並說明 contention 本身是 emulator 不模擬的 production 特性">Distributed counter lab</a>。</p>
<h2 id="引用路徑">引用路徑</h2>
<ul>
<li>上游：<a href="/blog/backend/01-database/vendors/firestore/hands-on/" data-link-title="Firestore Hands-on 操作路線" data-link-desc="用 Firebase Emulator Suite 在本地演練 Firestore：emulator quickstart、Security Rules 單元測試、distributed counter 分片計數，全程零雲端成本、可重跑、產出可驗證 artifact">Firestore Hands-on 操作路線</a></li>
<li>Deep article：<a href="/blog/backend/01-database/vendors/firestore/security-rules-authz-modeling/" data-link-title="Firestore Security Rules 授權建模與可測試化：把規則當程式碼治理" data-link-desc="Firestore client 直連模型把整個授權控制面壓在 Security Rules 這套 DSL 裡；本文展開規則的求值模型、把授權拆成可組合 function、用 emulator 寫單元測試、五個把規則寫成資安漏洞的 production 踩坑，以及規則複雜度撞牆時把授權拉回後端的邊界">Security Rules 授權建模</a></li>
<li>官方：<a href="https://firebase.google.com/docs/cli">Install Firebase CLI</a>、<a href="https://firebase.google.com/docs/emulator-suite/connect_firestore">Connect to Firestore emulator</a></li>
</ul>
]]></content:encoded></item><item><title>flutter devices 卡住的訊號：device 數從 N 變 N-1 與 emulator 半活</title><link>https://tarrragon.github.io/blog/work-log/flutter-devices-%E5%8D%A1%E4%BD%8F%E7%9A%84%E8%A8%8A%E8%99%9Fdevice-%E6%95%B8%E5%BE%9E-n-%E8%AE%8A-n-1-%E8%88%87-emulator-%E5%8D%8A%E6%B4%BB/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/work-log/flutter-devices-%E5%8D%A1%E4%BD%8F%E7%9A%84%E8%A8%8A%E8%99%9Fdevice-%E6%95%B8%E5%BE%9E-n-%E8%AE%8A-n-1-%E8%88%87-emulator-%E5%8D%8A%E6%B4%BB/</guid><description>&lt;p>&lt;code>flutter devices&lt;/code> 卡住時，最有用的訊號是「device 清單是否穩定」。這次的關鍵訊號是連續兩次掃描從 &lt;code>Found 4 connected devices&lt;/code> 變成 &lt;code>Found 3 connected devices&lt;/code>，再加上 &lt;code>Error -2 retrieving device properties for sdk gphone64 arm64&lt;/code>。這代表 ADB server 看得到某個 emulator entry，但對該 entry 的 property 查詢已經不穩定。&lt;/p>
&lt;p>這類狀態可以稱為 Android emulator 半活（zombie）：emulator host process 還在、ADB 清單仍殘留 device，但 emulator 內的 &lt;code>adbd&lt;/code> 或 Android system 已停止回應。Flutter 在掃描階段會對每個 Android device 查 properties，掃描到這個半活 device 就卡在 timeout。&lt;/p>
&lt;hr>
&lt;h2 id="事故場景">事故場景&lt;/h2>
&lt;p>事故場景的核心是「Flutter 指令看似卡住，其實卡在下游 device property 查詢」。連續跑 &lt;code>flutter devices&lt;/code> 時，輸出長這樣：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">$ flutter devices
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">Found 4 connected devices:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">Error -2 retrieving device properties for sdk gphone64 arm64:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">[卡住]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">$ flutter devices
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">Found 3 connected devices:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">[繼續卡]&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這段輸出有兩個值得注意的點：&lt;/p>
&lt;ol>
&lt;li>&lt;code>Error -2 retrieving device properties for sdk gphone64 arm64:&lt;/code> 訊息出現後仍繼續等待，代表 Flutter 沒有在第一個 device 失敗時 fail-fast&lt;/li>
&lt;li>第一次 &lt;code>Found 4&lt;/code>、第二次 &lt;code>Found 3&lt;/code>，代表 device 數在兩次掃描之間自己少了 1&lt;/li>
&lt;/ol>
&lt;p>&lt;code>sdk gphone64 arm64&lt;/code> 是 Android Studio AVD 預設模板（Google Phone 64-bit ARM）建出來的 emulator 顯示名稱、macOS 上跑 Android system image 都會看到這個。&lt;/p>
&lt;h3 id="為什麼計數變化是關鍵徵兆">為什麼計數變化是關鍵徵兆&lt;/h3>
&lt;p>device 數從 4 變 3，代表 ADB 對某個 emulator 的狀態判斷在兩次查詢之間變了。ADB server 內部追蹤每個 device 的狀態（&lt;code>device&lt;/code> / &lt;code>offline&lt;/code> / &lt;code>unauthorized&lt;/code> / &lt;code>no permissions&lt;/code>）；半活 emulator 在第一次掃描時仍被列在 &lt;code>Found 4&lt;/code>，第二次掃描時可能已被標成 offline 或從候選清單移除，所以掉到 &lt;code>Found 3&lt;/code>。&lt;/p>
&lt;p>判讀訊號是「同一條 list 指令連跑兩次，device 數或 device 狀態自己變」。正常穩定狀態下，清單應該保持一致；清單漂移代表 ADB server 對某個 entry 的看法不穩定，下一步要先找出那個 entry，再決定是否重啟 ADB 或 emulator。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼-flutter-devices-會卡住">為什麼 flutter devices 會卡住&lt;/h2>
&lt;p>&lt;code>flutter devices&lt;/code> 的責任是把每個候選 device 補成 Flutter 可用的 target，而不只是印出 &lt;code>adb devices&lt;/code> 的結果。Flutter 對每個 ADB 看得到的 Android device 還要做幾件事：&lt;/p>
&lt;ol>
&lt;li>跑 &lt;code>adb shell getprop ro.product.cpu.abi&lt;/code> 拉 ABI&lt;/li>
&lt;li>跑 &lt;code>adb shell getprop ro.build.version.sdk&lt;/code> 拉 SDK level&lt;/li>
&lt;li>跑 &lt;code>adb shell getprop ro.product.model&lt;/code> 拉裝置型號&lt;/li>
&lt;li>視情況跑 &lt;code>adb shell&lt;/code> 其他指令確認 Flutter 支援度&lt;/li>
&lt;/ol>
&lt;p>這些是同步、序列化、有 timeout 的呼叫；timeout 通常設得相對寬鬆，讓慢一點的真機也能跑通。當其中一個 device 是 zombie 狀態：&lt;/p></description><content:encoded><![CDATA[<p><code>flutter devices</code> 卡住時，最有用的訊號是「device 清單是否穩定」。這次的關鍵訊號是連續兩次掃描從 <code>Found 4 connected devices</code> 變成 <code>Found 3 connected devices</code>，再加上 <code>Error -2 retrieving device properties for sdk gphone64 arm64</code>。這代表 ADB server 看得到某個 emulator entry，但對該 entry 的 property 查詢已經不穩定。</p>
<p>這類狀態可以稱為 Android emulator 半活（zombie）：emulator host process 還在、ADB 清單仍殘留 device，但 emulator 內的 <code>adbd</code> 或 Android system 已停止回應。Flutter 在掃描階段會對每個 Android device 查 properties，掃描到這個半活 device 就卡在 timeout。</p>
<hr>
<h2 id="事故場景">事故場景</h2>
<p>事故場景的核心是「Flutter 指令看似卡住，其實卡在下游 device property 查詢」。連續跑 <code>flutter devices</code> 時，輸出長這樣：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">$ flutter devices
</span></span><span class="line"><span class="ln">2</span><span class="cl">Found 4 connected devices:
</span></span><span class="line"><span class="ln">3</span><span class="cl">Error -2 retrieving device properties for sdk gphone64 arm64:
</span></span><span class="line"><span class="ln">4</span><span class="cl">[卡住]
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl">$ flutter devices
</span></span><span class="line"><span class="ln">7</span><span class="cl">Found 3 connected devices:
</span></span><span class="line"><span class="ln">8</span><span class="cl">[繼續卡]</span></span></code></pre></div><p>這段輸出有兩個值得注意的點：</p>
<ol>
<li><code>Error -2 retrieving device properties for sdk gphone64 arm64:</code> 訊息出現後仍繼續等待，代表 Flutter 沒有在第一個 device 失敗時 fail-fast</li>
<li>第一次 <code>Found 4</code>、第二次 <code>Found 3</code>，代表 device 數在兩次掃描之間自己少了 1</li>
</ol>
<p><code>sdk gphone64 arm64</code> 是 Android Studio AVD 預設模板（Google Phone 64-bit ARM）建出來的 emulator 顯示名稱、macOS 上跑 Android system image 都會看到這個。</p>
<h3 id="為什麼計數變化是關鍵徵兆">為什麼計數變化是關鍵徵兆</h3>
<p>device 數從 4 變 3，代表 ADB 對某個 emulator 的狀態判斷在兩次查詢之間變了。ADB server 內部追蹤每個 device 的狀態（<code>device</code> / <code>offline</code> / <code>unauthorized</code> / <code>no permissions</code>）；半活 emulator 在第一次掃描時仍被列在 <code>Found 4</code>，第二次掃描時可能已被標成 offline 或從候選清單移除，所以掉到 <code>Found 3</code>。</p>
<p>判讀訊號是「同一條 list 指令連跑兩次，device 數或 device 狀態自己變」。正常穩定狀態下，清單應該保持一致；清單漂移代表 ADB server 對某個 entry 的看法不穩定，下一步要先找出那個 entry，再決定是否重啟 ADB 或 emulator。</p>
<hr>
<h2 id="為什麼-flutter-devices-會卡住">為什麼 flutter devices 會卡住</h2>
<p><code>flutter devices</code> 的責任是把每個候選 device 補成 Flutter 可用的 target，而不只是印出 <code>adb devices</code> 的結果。Flutter 對每個 ADB 看得到的 Android device 還要做幾件事：</p>
<ol>
<li>跑 <code>adb shell getprop ro.product.cpu.abi</code> 拉 ABI</li>
<li>跑 <code>adb shell getprop ro.build.version.sdk</code> 拉 SDK level</li>
<li>跑 <code>adb shell getprop ro.product.model</code> 拉裝置型號</li>
<li>視情況跑 <code>adb shell</code> 其他指令確認 Flutter 支援度</li>
</ol>
<p>這些是同步、序列化、有 timeout 的呼叫；timeout 通常設得相對寬鬆，讓慢一點的真機也能跑通。當其中一個 device 是 zombie 狀態：</p>
<ul>
<li><code>adb shell getprop ...</code> 送出後，ADB 把指令轉發給 emulator 內的 <code>adbd</code></li>
<li><code>adbd</code> 收到了但 Android system 沒回應，或 emulator process 整個卡住沒在處理 ADB request</li>
<li>Flutter 端等 timeout、再 retry、再等更長 timeout，看起來就是「整個指令卡住」</li>
</ul>
<p><code>Error -2 retrieving device properties</code> 是其中一次嘗試 timeout 拿到的訊息（<code>-2</code> 是 Dart <code>ProcessException</code> 對應 <code>adb</code> exit code 的內部映射）。Flutter 仍會繼續掃描其他 device，所以使用者看到的是「印出錯誤訊息 + 繼續卡」。</p>
<hr>
<h2 id="為什麼是半活狀態">為什麼是半活狀態</h2>
<p>Android emulator 在 macOS 上的結構大致是：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">qemu-system-aarch64 (host process)
</span></span><span class="line"><span class="ln">2</span><span class="cl">  ├─ Android kernel
</span></span><span class="line"><span class="ln">3</span><span class="cl">  ├─ Android system services
</span></span><span class="line"><span class="ln">4</span><span class="cl">  └─ adbd (在 emulator 內部，跟 host ADB server 對接)</span></span></code></pre></div><p>半活狀態指的是「host process 還在，但 device 內部服務已無法完成 ADB request」。完全正常時 emulator 跑得動、ADB 也通；完全退出時 emulator process 已結束、ADB 清單看不到它。半活介於兩者之間：</p>
<ul>
<li>qemu host process 還在（活著）</li>
<li>emulator 內的某個環節卡住（Android system 沒在 schedule、或 adbd 卡在某個 mutex）</li>
<li>ADB server 還記得有這個 device，尚未穩定 evict</li>
<li>任何 <code>adb shell</code> 指令都打不通</li>
</ul>
<p>常見成因：</p>
<ul>
<li><strong>Quick Boot snapshot 還原失敗或部分還原</strong>——AVD 預設關機是 quick boot（存 snapshot），下次開機從 snapshot 還原；snapshot 跟當前 host kernel / hypervisor 狀態不相容時會半開機</li>
<li><strong>macOS 從 sleep 喚醒後 hypervisor framework 重置</strong>——emulator 是用 Hypervisor.framework，喚醒後虛擬 CPU 可能停在奇怪 state</li>
<li><strong>host 端記憶體壓力導致 emulator 被 swap 嚴重</strong>——表面看起來像卡，其實是在等 page fault</li>
</ul>
<p>這一層的操作目標是恢復工具鏈，而不是追到每個 emulator 內部 race condition。若症狀符合清單漂移與 property 查詢 timeout，先按恢復順序處理；只有反覆發生時，再追 AVD snapshot、system image 或 host 資源壓力。</p>
<hr>
<h2 id="恢復順序從輕到重">恢復順序（從輕到重）</h2>
<p>恢復順序的核心是先重置最小邊界，再逐層擴大。每一步都要重新跑一次 <code>flutter devices</code> 或 <code>adb devices</code>，確認是否已經恢復，避免直接砍掉 emulator 或清資料。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 1. 看 ADB 對每個 device 的狀態</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">adb devices
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 看到 offline / no device / unauthorized 等異常狀態 → 先鎖定該 device</span></span></span></code></pre></div><p>如果有 device 顯示 <code>offline</code>，或正常列出但實際打不通，先重啟 ADB server：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 2. 重啟 ADB server（只重置 host 端 ADB session）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">adb kill-server <span class="o">&amp;&amp;</span> adb start-server
</span></span><span class="line"><span class="ln">3</span><span class="cl">adb devices
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 多數狀況下，ADB 重啟後對該 device 的查詢會 fail-fast，flutter devices 會恢復</span></span></span></code></pre></div><p>如果 ADB 重啟後仍打不通該 emulator，再處理 emulator process：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 3. 對特定 emulator 發 emu kill（讓它優雅關閉）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">adb -s emulator-5554 emu <span class="nb">kill</span>   <span class="c1"># 把 5554 換成實際 port</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 4. 還在的話，終止 qemu process</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">pkill -f qemu-system-aarch64</span></span></code></pre></div><p>長期修復路由是清掉不穩定的 snapshot。開 Android Studio → <strong>AVD Manager</strong> → 該 emulator 旁邊的小箭頭 → <strong>Cold Boot Now</strong>（避免 Quick Boot）。如果冷啟動後仍反覆壞，選 <strong>Wipe Data</strong> 把 snapshot 與 emulator 內資料整個清掉。</p>
<hr>
<h2 id="通用診斷思維">通用診斷思維</h2>
<p>工具鏈卡住的診斷核心是先區分「上游 CLI 壞掉」還是「下游 target 沒回應」。<code>flutter</code> / <code>adb</code> 指令卡住時，先用清單穩定性與 device 識別碼定位下游狀態，再決定重啟邊界。</p>
<ol>
<li><strong>觀察「同一指令連跑兩次結果是否一致」</strong>：不一致（device 數變、訊息變）等於某層狀態不穩定</li>
<li><strong>訊息裡有 device 識別碼就釘住它</strong>：<code>sdk gphone64 arm64</code>、<code>emulator-5554</code>、序號等都是 ADB 層的識別，可直接拿來 <code>adb -s &lt;id&gt; ...</code> 局部診斷</li>
<li><strong>從外往內排除</strong>：ADB server → 個別 device → emulator process → emulator 內 system，逐層重啟</li>
<li><strong>重啟邊界越大、副作用越大</strong>：<code>adb kill-server</code> 只影響 ADB session（其他 device 連線會斷一下），<code>pkill qemu</code> 直接砍 emulator，<code>Wipe Data</code> 連 emulator 內的資料都清。能用輕量手段解決就停在那層</li>
</ol>
<hr>
<h2 id="操作判準">操作判準</h2>
<ol>
<li><strong>「device 數兩次掃描之間自己變」是 zombie emulator 的關鍵徵兆</strong>：計數變化代表 ADB 內部狀態不穩定</li>
<li><strong><code>Error -2 retrieving device properties</code> 是 property 查詢失敗訊號</strong>：Flutter 仍可能繼續處理其他 device，結果是「印出錯誤訊息但繼續卡」</li>
<li><strong><code>adb kill-server &amp;&amp; adb start-server</code> 是輕量首選</strong>：它只重置 ADB session，不動 emulator 本身，多數狀況下可讓壞 device fail-fast</li>
<li><strong>半活狀態跟 application code 層級不同</strong>：先把工具鏈狀態釐清，再回到剛改的程式碼</li>
</ol>
<hr>
<h2 id="適用範圍">適用範圍</h2>
<p>這個診斷思維不限於 Android emulator：</p>
<ul>
<li>iOS Simulator 卡住時 <code>xcrun simctl list</code> 印不出來——同樣的「指令卡 + 訊息看似 fatal 但 process 仍存在」結構</li>
<li><code>flutter devices</code> 對任何 device（含 iOS、Web、desktop）的查詢都會走類似的「列出 → 逐個 query property」流程、任一層卡都會表現為類似症狀</li>
<li>廣義地說，任何「server 維護一份 client 清單 + 對每個 client 做同步呼叫」的架構（k8s <code>kubectl get pods</code> 對 zombie node、docker <code>docker ps</code> 對掛掉的 container runtime 等）都有同款 failure mode</li>
</ul>
<p>辨認規則一致：<strong>list 指令連跑兩次結果不一致 → 維護清單的 server 對某個 entry 的看法不穩定 → 找出那個 entry 局部處理</strong>。這條規則的邊界是：如果清單穩定但操作失敗，問題更可能在該 target 的權限、版本或 runtime 狀態，需要改走對應工具的細部診斷。</p>
]]></content:encoded></item></channel></rss>